From 295e46731e7589ffd54c64b08e8b1f683e92102c Mon Sep 17 00:00:00 2001 From: Benjamin Broadaway <4554569+benbroadaway@users.noreply.github.com> Date: Wed, 19 Jun 2024 19:49:09 -0500 Subject: [PATCH 1/3] jira: enable tests --- tasks/jira/pom.xml | 40 ++- .../concord/plugins/jira/Constants.java | 33 ++ .../concord/plugins/jira/JiraClient.java | 109 +++--- .../concord/plugins/jira/JiraClientCfg.java | 27 ++ .../concord/plugins/jira/JiraCredentials.java | 6 + .../concord/plugins/jira/JiraHttpClient.java | 63 ++++ .../plugins/jira/JiraHttpClientFactory.java | 50 +++ .../concord/plugins/jira/JiraTask.java | 50 ++- .../concord/plugins/jira/JiraTaskCommon.java | 84 ++--- .../plugins/jira/NativeJiraHttpClient.java | 178 ++++++++++ .../concord/plugins/jira/TaskParams.java | 29 +- .../concord/plugins/jira/v2/JiraTaskV2.java | 27 +- .../plugins/jira/AbstractWiremockTest.java | 264 +++++++++++++++ .../concord/plugins/jira/CommonTest.java | 316 ++++++++++++++++++ .../concord/plugins/jira/JiraClientTest.java | 30 ++ .../concord/plugins/jira/JiraTaskTest.java | 210 +++--------- .../jira/NativeJiraHttpClientTest.java | 30 ++ .../concord/plugins/jira/TaskParamsTest.java | 63 ++++ .../plugins/jira/v2/JiraTaskV2Test.java | 102 ++++++ 19 files changed, 1423 insertions(+), 288 deletions(-) create mode 100644 tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/Constants.java create mode 100644 tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraHttpClient.java create mode 100644 tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraHttpClientFactory.java create mode 100644 tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/NativeJiraHttpClient.java create mode 100644 tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/AbstractWiremockTest.java create mode 100644 tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/CommonTest.java create mode 100644 tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/JiraClientTest.java create mode 100644 tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/NativeJiraHttpClientTest.java create mode 100644 tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/TaskParamsTest.java create mode 100644 tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/v2/JiraTaskV2Test.java diff --git a/tasks/jira/pom.xml b/tasks/jira/pom.xml index 37dad46b..e906b6a5 100644 --- a/tasks/jira/pom.xml +++ b/tasks/jira/pom.xml @@ -13,7 +13,7 @@ takari-jar - true + false @@ -32,6 +32,11 @@ concord-common provided + + com.walmartlabs.concord + concord-client2 + provided + javax.inject javax.inject @@ -43,10 +48,31 @@ provided - com.google.code.gson - gson - ${gson.version} + com.fasterxml.jackson.core + jackson-databind + provided + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + com.fasterxml.jackson.core + jackson-core + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + provided + + com.google.code.findbugs + jsr305 + provided + + com.squareup.okhttp okhttp @@ -68,6 +94,12 @@ mockito-core test + + org.mockito + mockito-junit-jupiter + 4.9.0 + test + diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/Constants.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/Constants.java new file mode 100644 index 00000000..1ccd42c7 --- /dev/null +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/Constants.java @@ -0,0 +1,33 @@ +package com.walmartlabs.concord.plugins.jira; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2024 Walmart Inc., Concord Authors + * ----- + * 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. + * ===== + */ + +import java.util.UUID; + +public class Constants { + + private Constants() { + throw new IllegalStateException("instantiation is not allowed"); + } + + static final String BOUNDARY = UUID.randomUUID().toString(); + +} diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraClient.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraClient.java index 1c7f4f35..1a8bf84d 100644 --- a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraClient.java +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraClient.java @@ -20,31 +20,39 @@ * ===== */ -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; -import com.squareup.okhttp.*; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.squareup.okhttp.Call; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.MultipartBuilder; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; +import com.squareup.okhttp.ResponseBody; import java.io.File; import java.io.IOException; -import java.lang.reflect.Type; +import java.net.URI; import java.nio.file.Files; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -public class JiraClient { +public class JiraClient implements JiraHttpClient { - private static final OkHttpClient client = new OkHttpClient(); - private static final Gson gson = new GsonBuilder().create(); - - private static final TypeToken> MAP_TYPE_TOKEN = new TypeToken>() { - }; - private static final TypeToken>> LIST_OF_MAPS_TYPE_TOKEN = new TypeToken>>() { - }; + private static final ObjectMapper MAPPER = new ObjectMapper() + .registerModule(new Jdk8Module()); + static final JavaType MAP_TYPE = MAPPER.getTypeFactory() + .constructMapType(HashMap.class, String.class, Object.class); + private static final JavaType LIST_OF_MAPS_TYPE = MAPPER.getTypeFactory() + .constructCollectionType(List.class, MAP_TYPE); + private static final OkHttpClient client = new OkHttpClient(); private final JiraClientCfg cfg; - private String url; + private URI uri; private int successCode; private String auth; @@ -52,74 +60,82 @@ public JiraClient(JiraClientCfg cfg) { this.cfg = cfg; } - public JiraClient url(String url) { - this.url = url; + @Override + public JiraHttpClient url(String url) { + this.uri = URI.create(url); return this; } - public JiraClient successCode(int successCode) { + @Override + public JiraHttpClient successCode(int successCode) { this.successCode = successCode; return this; } - public JiraClient jiraAuth(String auth) { + @Override + public JiraHttpClient jiraAuth(String auth) { this.auth = auth; return this; } + @Override public Map get() throws IOException { Request request = requestBuilder(auth) - .url(url) + .url(uri.toURL()) .get() .build(); - return call(request, MAP_TYPE_TOKEN.getType()); + return call(request, MAP_TYPE); } + @Override public Map post(Map data) throws IOException { RequestBody body = RequestBody.create( - MediaType.parse("application/json; charset=utf-8"), gson.toJson(data)); + MediaType.parse("application/json; charset=utf-8"), MAPPER.writeValueAsString(data)); Request request = requestBuilder(auth) - .url(url) + .url(uri.toURL()) .post(body) .build(); - return call(request, MAP_TYPE_TOKEN.getType()); + return call(request, MAP_TYPE); } + @Override public void post(File file) throws IOException { - MultipartBuilder b = new MultipartBuilder().type(MultipartBuilder.FORM); + MultipartBuilder b = new MultipartBuilder(Constants.BOUNDARY).type(MultipartBuilder.FORM); b.addFormDataPart("file", file.getName(), RequestBody.create(MediaType.parse("application/octet-stream"), Files.readAllBytes(file.toPath()))); RequestBody body = b.build(); Request request = requestBuilder(auth) .header("X-Atlassian-Token", "nocheck") - .url(url) + .url(uri.toURL()) .post(body) .build(); - call(request, LIST_OF_MAPS_TYPE_TOKEN.getType()); + call(request, LIST_OF_MAPS_TYPE); } + @Override public void put(Map data) throws IOException { RequestBody body = RequestBody.create( - MediaType.parse("application/json; charset=utf-8"), gson.toJson(data)); + MediaType.parse("application/json; charset=utf-8"), MAPPER.writeValueAsString(data)); Request request = requestBuilder(auth) - .url(url) + .url(uri.toURL()) .put(body) .build(); - call(request, MAP_TYPE_TOKEN.getType()); + call(request, MAP_TYPE); } + @Override public void delete() throws IOException { Request request = requestBuilder(auth) - .url(url) + .url(uri.toURL()) .delete() .build(); - call(request, MAP_TYPE_TOKEN.getType()); + call(request, MAP_TYPE); } private static Request.Builder requestBuilder(String auth) { @@ -128,7 +144,8 @@ private static Request.Builder requestBuilder(String auth) { .addHeader("Accept", "application/json"); } - private T call(Request request, Type returnType) throws IOException { + + T call(Request request, JavaType returnType) throws IOException { setClientTimeoutParams(cfg); Call call = client.newCall(request); @@ -141,29 +158,13 @@ private T call(Request request, Type returnType) throws IOException { } int statusCode = response.code(); - assertResponseCode(statusCode, results, successCode); - - return gson.fromJson(results, returnType); - } - } + JiraHttpClient.assertResponseCode(statusCode, results, successCode); - private static void assertResponseCode(int code, String result, int successCode) { - if (code == successCode) { - return; - } - - if (code == 400) { - throw new RuntimeException("input is invalid (e.g. missing required fields, invalid values). Here are the full error details: " + result); - } else if (code == 401) { - throw new RuntimeException("User is not authenticated. Here are the full error details: " + result); - } else if (code == 403) { - throw new RuntimeException("User does not have permission to perform request. Here are the full error details: " + result); - } else if (code == 404) { - throw new RuntimeException("Issue does not exist. Here are the full error details: " + result); - } else if (code == 500) { - throw new RuntimeException("Internal Server Error. Here are the full error details" + result); - } else { - throw new RuntimeException("Error: " + result); + if (results == null || statusCode == 204) { + return null; + } else { + return MAPPER.readValue(results, returnType); + } } } diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraClientCfg.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraClientCfg.java index 1e526ed9..18cf7dfc 100644 --- a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraClientCfg.java +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraClientCfg.java @@ -33,4 +33,31 @@ default long readTimeout() { default long writeTimeout() { return 30L; } + + default HttpVersion httpProtocolVersion() { + return HttpVersion.DEFAULT; + } + + enum HttpVersion { + HTTP_1_1("http/1.1"), + HTTP_2("http/2.0"), + DEFAULT("default"); + + private final String value; + + HttpVersion(String value) { + this.value = value; + } + + public static HttpVersion from(String val) { + for (HttpVersion version : HttpVersion.values()) { + if (version.value.equals(val)) { + return version; + } + } + + return DEFAULT; + } + + } } diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraCredentials.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraCredentials.java index 84338bb4..645eaa88 100644 --- a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraCredentials.java +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraCredentials.java @@ -20,6 +20,8 @@ * ===== */ +import java.util.Base64; + public class JiraCredentials { private final String username; @@ -37,4 +39,8 @@ public String username() { public String password() { return password; } + + public String authHeaderValue() { + return "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); + } } diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraHttpClient.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraHttpClient.java new file mode 100644 index 00000000..f710327b --- /dev/null +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraHttpClient.java @@ -0,0 +1,63 @@ +package com.walmartlabs.concord.plugins.jira; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2024 Walmart Inc., Concord Authors + * ----- + * 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. + * ===== + */ + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +public interface JiraHttpClient { + JiraHttpClient url(String url); + + JiraHttpClient successCode(int successCode); + + JiraHttpClient jiraAuth(String auth); + + Map get() throws IOException; + + Map post(Map data) throws IOException; + + void post(File file) throws IOException; + + void put(Map data) throws IOException; + + void delete() throws IOException; + + static void assertResponseCode(int code, String result, int successCode) { + if (code == successCode) { + return; + } + + if (code == 400) { + throw new IllegalStateException("input is invalid (e.g. missing required fields, invalid values). Here are the full error details: " + result); + } else if (code == 401) { + throw new IllegalStateException("User is not authenticated. Here are the full error details: " + result); + } else if (code == 403) { + throw new IllegalStateException("User does not have permission to perform request. Here are the full error details: " + result); + } else if (code == 404) { + throw new IllegalStateException("Issue does not exist. Here are the full error details: " + result); + } else if (code == 500) { + throw new IllegalStateException("Internal Server Error. Here are the full error details" + result); + } else { + throw new IllegalStateException("Error: " + result); + } + } +} diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraHttpClientFactory.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraHttpClientFactory.java new file mode 100644 index 00000000..51d6d4c4 --- /dev/null +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraHttpClientFactory.java @@ -0,0 +1,50 @@ +package com.walmartlabs.concord.plugins.jira; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2024 Walmart Inc., Concord Authors + * ----- + * 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. + * ===== + */ + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class JiraHttpClientFactory { + + private static final Logger log = LoggerFactory.getLogger(JiraHttpClientFactory.class); + + private JiraHttpClientFactory() { + throw new IllegalStateException("instantiation is not allowed"); + } + + public static JiraHttpClient create(JiraClientCfg cfg) { + try { + return new NativeJiraHttpClient(cfg); + } catch (NoClassDefFoundError e) { + // client2 may not exist + log.info("Falling back to okhttp client"); + } + + try { + return new JiraClient(cfg); + } catch (Exception e) { + // that's very unexpected as long as okhttp is still allowed + throw new IllegalStateException("No jira http client found"); + } + } + +} diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraTask.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraTask.java index 7ed81a74..b36714d5 100644 --- a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraTask.java +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraTask.java @@ -21,7 +21,12 @@ */ import com.walmartlabs.concord.runtime.v2.sdk.Variables; -import com.walmartlabs.concord.sdk.*; +import com.walmartlabs.concord.sdk.Context; +import com.walmartlabs.concord.sdk.ContextUtils; +import com.walmartlabs.concord.sdk.InjectVariable; +import com.walmartlabs.concord.sdk.MapUtils; +import com.walmartlabs.concord.sdk.SecretService; +import com.walmartlabs.concord.sdk.Task; import javax.inject.Inject; import javax.inject.Named; @@ -47,32 +52,53 @@ public JiraTask(SecretService secretService) { @Override public void execute(Context ctx) { - Map result = delegate(ctx).execute(TaskParams.of(new ContextVariables(ctx), defaults)); + JiraSecretService jiraSecretService = getSecretService(ctx); + Map result = delegate(jiraSecretService) + .execute(TaskParams.of(new ContextVariables(ctx), defaults)); result.forEach(ctx::setVariable); } public String getStatus(@InjectVariable("context") Context ctx, String issueKey) { Variables vars = TaskParams.merge(new ContextVariables(ctx), defaults); - - Map result = delegate(ctx).execute(new TaskParams.CurrentStatusParams(vars) { - @Override - public String issueKey() { - return issueKey; - } - }); + JiraSecretService jiraSecretService = getSecretService(ctx); + Map result = delegate(jiraSecretService) + .execute(new TaskParams.CurrentStatusParams(vars) { + @Override + public String issueKey() { + return issueKey; + } + }); return MapUtils.getString(result, JIRA_ISSUE_STATUS_KEY); } - private JiraTaskCommon delegate(Context ctx) { - return new JiraTaskCommon((orgName, secretName, password) -> { + JiraTaskCommon delegate(JiraSecretService jiraSecretService) { + return new JiraTaskCommon(jiraSecretService); + } + + private JiraSecretService getSecretService(Context ctx) { + return new V1SecretService(secretService, ctx); + } + + static class V1SecretService implements JiraSecretService { + private final SecretService secretService; + + public V1SecretService(SecretService secretService, Context ctx) { + this.secretService = secretService; + this.ctx = ctx; + } + + private final Context ctx; + + @Override + public JiraCredentials exportCredentials(String orgName, String secretName, String password) throws Exception { UUID txId = ContextUtils.getTxId(ctx); Path workDir = ContextUtils.getWorkDir(ctx); Map result1 = secretService.exportCredentials(ctx, txId.toString(), workDir.toString(), orgName, secretName, password); return new JiraCredentials(result1.get("username"), result1.get("password")); - }); + } } private static class ContextVariables implements Variables { diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraTaskCommon.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraTaskCommon.java index f7713911..bbc7a687 100644 --- a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraTaskCommon.java +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraTaskCommon.java @@ -20,7 +20,6 @@ * ===== */ -import com.squareup.okhttp.Credentials; import com.walmartlabs.concord.common.ConfigurationUtils; import com.walmartlabs.concord.sdk.MapUtils; import org.slf4j.Logger; @@ -118,7 +117,7 @@ public Map execute(TaskParams in) { } - private Map createIssue(CreateIssueParams in) { + Map createIssue(CreateIssueParams in) { String projectKey = in.projectKey(); String summary = in.summary(); String description = in.description(); @@ -163,28 +162,20 @@ private Map createIssue(CreateIssueParams in) { objMain.put("assignee", assignee); } - if (customFieldsTypeKv != null && !customFieldsTypeKv.isEmpty()) { - for (Map.Entry e : customFieldsTypeKv.entrySet()) { - objMain.put(e.getKey(), String.valueOf(e.getValue())); - } - } + objMain.putAll(customFieldsTypeKv); + objMain.putAll(customFieldsTypeAtt); - if (customFieldsTypeAtt != null && !customFieldsTypeAtt.isEmpty()) { - for (Map.Entry e : customFieldsTypeAtt.entrySet()) { - objMain.put(e.getKey(), e.getValue()); - } - } Map objFields = Collections.singletonMap("fields", objMain); log.info("Creating new issue in '{}'...", projectKey); - Map results = new JiraClient(in) + Map results = getClient(in) .url(in.jiraUrl() + "issue/") .jiraAuth(buildAuth(in)) .successCode(201) .post(objFields); - issueId = results.get("key").toString().replaceAll("\"", ""); + issueId = results.get("key").toString().replace("\"", ""); log.info("Issue #{} created in Project# '{}'", issueId, projectKey); } catch (Exception e) { throw new RuntimeException("Error occurred while creating an issue: " + e.getMessage(), e); @@ -193,11 +184,11 @@ private Map createIssue(CreateIssueParams in) { return Collections.singletonMap(JIRA_ISSUE_ID_KEY, issueId); } - private Map createSubTask(CreateSubTaskParams in) { + Map createSubTask(CreateSubTaskParams in) { return createIssue(in); } - private Map createComponent(CreateComponentParams in) { + Map createComponent(CreateComponentParams in) { String projectKey = in.projectKey(); String componentName = in.componentName(); @@ -206,14 +197,14 @@ private Map createComponent(CreateComponentParams in) { m.put("name", componentName); m.put("project", projectKey); - Map results = new JiraClient(in) + Map results = getClient(in) .url(in.jiraUrl() + "component/") .jiraAuth(buildAuth(in)) .successCode(201) .post(m); String componentId = results.get("id").toString(); - componentId = componentId.replaceAll("\"", ""); + componentId = componentId.replace("\"", ""); log.info("Component '{}' created successfully and its Id is '{}'", componentName, componentId); return Collections.singletonMap(JIRA_COMPONENT_ID_KEY, componentId); } catch (Exception e) { @@ -221,11 +212,11 @@ private Map createComponent(CreateComponentParams in) { } } - private void deleteComponent(DeleteComponentParams in) { + void deleteComponent(DeleteComponentParams in) { int componentId = in.componentId(); try { - new JiraClient(in) + getClient(in) .url(in.jiraUrl() + "component/" + componentId) .jiraAuth(buildAuth(in)) .successCode(204) @@ -237,7 +228,7 @@ private void deleteComponent(DeleteComponentParams in) { } } - private void addAttachment(AddAttachmentParams in) { + void addAttachment(AddAttachmentParams in) { String issueKey = in.issueKey(); String filePath = in.filePath(); @@ -247,7 +238,7 @@ private void addAttachment(AddAttachmentParams in) { } try { - new JiraClient(in) + getClient(in) .url(in.jiraUrl() + "issue/" + issueKey + "/attachments") .successCode(200) .jiraAuth(buildAuth(in)) @@ -257,7 +248,7 @@ private void addAttachment(AddAttachmentParams in) { } } - private void addComment(AddCommentParams in) { + void addComment(AddCommentParams in) { String issueKey = in.issueKey(); String comment = in.comment(); boolean debug = in.debug(); @@ -265,7 +256,7 @@ private void addComment(AddCommentParams in) { try { Map m = Collections.singletonMap("body", comment); - new JiraClient(in) + getClient(in) .url(in.jiraUrl() + "issue/" + issueKey + "/comment") .jiraAuth(buildAuth(in)) .successCode(201) @@ -281,7 +272,7 @@ private void addComment(AddCommentParams in) { } } - private void transition(TransitionParams in) { + void transition(TransitionParams in) { String issueKey = in.issueKey(); String transitionId = Integer.toString(in.transitionId(-1)); String transitionComment = in.transitionComment(); @@ -300,22 +291,13 @@ private void transition(TransitionParams in) { Map objupdate = Collections.singletonMap("update", objComment); Map objMain = new HashMap<>(); - if (transitionFieldsTypeKv != null && !transitionFieldsTypeKv.isEmpty()) { - for (Map.Entry e : transitionFieldsTypeKv.entrySet()) { - objMain.put(e.getKey(), String.valueOf(e.getValue())); - } - } - - if (transitionFieldsTypeAtt != null && !transitionFieldsTypeAtt.isEmpty()) { - for (Map.Entry e : transitionFieldsTypeAtt.entrySet()) { - objMain.put(e.getKey(), e.getValue()); - } - } + transitionFieldsTypeKv.forEach((k, v) -> objMain.put(k, String.valueOf(v))); + objMain.putAll(transitionFieldsTypeAtt); Map objFields = Collections.singletonMap("fields", objMain); objupdate = ConfigurationUtils.deepMerge(objFields, ConfigurationUtils.deepMerge(objTransition, objupdate)); - new JiraClient(in) + getClient(in) .url(in.jiraUrl() + "issue/" + issueKey + "/transitions") .jiraAuth(buildAuth(in)) .successCode(204) @@ -327,11 +309,11 @@ private void transition(TransitionParams in) { } } - private void deleteIssue(DeleteIssueParams in) { + void deleteIssue(DeleteIssueParams in) { String issueKey = in.issueKey(); try { - new JiraClient(in) + getClient(in) .url(in.jiraUrl() + "issue/" + issueKey) .jiraAuth(buildAuth(in)) .successCode(204) @@ -343,18 +325,18 @@ private void deleteIssue(DeleteIssueParams in) { } } - private void updateIssue(UpdateIssueParams in) { + void updateIssue(UpdateIssueParams in) { String issueKey = in.issueKey(); Map fields = in.fields(); log.info("Updating {} fields for issue #{}", fields, issueKey); try { - new JiraClient(in) + getClient(in) .url(in.jiraUrl() + "issue/" + issueKey) .jiraAuth(buildAuth(in)) .successCode(204) - .put(Collections.singletonMap("fields", fields)); + .put(Map.of("fields", fields)); log.info("Issue #{} updated successfully.", issueKey); } catch (Exception e) { @@ -363,7 +345,7 @@ private void updateIssue(UpdateIssueParams in) { } @SuppressWarnings("unchecked") - private Map getIssues(GetIssuesParams in) { + Map getIssues(GetIssuesParams in) { String projectKey = in.projectKey(); String issueType = in.issueType(); String issueStatus = in.issueStatus(); @@ -389,7 +371,7 @@ private Map getIssues(GetIssuesParams in) { List fieldList = Collections.singletonList("key"); objMain.put("fields", fieldList); - Map results = new JiraClient(in) + Map results = getClient(in) .url(in.jiraUrl() + "search") .jiraAuth(buildAuth(in)) .successCode(200) @@ -423,7 +405,7 @@ private Map getIssues(GetIssuesParams in) { private String buildAuth(TaskParams in) { JiraCredentials c = credentials(in); - return Credentials.basic(c.username(), c.password()); + return c.authHeaderValue(); } private JiraCredentials credentials(TaskParams in) { @@ -453,7 +435,7 @@ private JiraCredentials getSecretData(Map input) { String password = MapUtils.getString(input, JIRA_PASSWORD_KEY); try { - return secretService.exportCredentials(org, secretName, password); + return getSecretService().exportCredentials(org, secretName, password); } catch (Exception e) { throw new RuntimeException("Error export credentials: " + e.getMessage()); } @@ -461,7 +443,7 @@ private JiraCredentials getSecretData(Map input) { private Map currentStatus(CurrentStatusParams in) { try { - Map results = new JiraClient(in) + Map results = getClient(in) .url(formatUrl(in.jiraUrl()) + "issue/" + in.issueKey() + "?fields=status") .jiraAuth(buildAuth(in)) .successCode(200) @@ -508,4 +490,12 @@ private String configureStatus(String projectKey, String issueType, String issue return jqlQuery; } + JiraHttpClient getClient(TaskParams in) { + return JiraHttpClientFactory.create(in); + } + + JiraSecretService getSecretService() { + return secretService; + } + } diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/NativeJiraHttpClient.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/NativeJiraHttpClient.java new file mode 100644 index 00000000..6377ec79 --- /dev/null +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/NativeJiraHttpClient.java @@ -0,0 +1,178 @@ +package com.walmartlabs.concord.plugins.jira; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2024 Walmart Inc., Concord Authors + * ----- + * 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. + * ===== + */ + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.walmartlabs.concord.client2.impl.MultipartBuilder; +import com.walmartlabs.concord.client2.impl.MultipartRequestBodyHandler; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class NativeJiraHttpClient implements JiraHttpClient { + + private static final ObjectMapper MAPPER = new ObjectMapper() + .registerModule(new Jdk8Module()); + static final JavaType MAP_TYPE = MAPPER.getTypeFactory() + .constructMapType(HashMap.class, String.class, Object.class); + private static final JavaType LIST_OF_MAPS_TYPE = MAPPER.getTypeFactory() + .constructCollectionType(List.class, MAP_TYPE); + private static final String CONTENT_TYPE = "Content-Type"; + + private final HttpClient client; + private final JiraClientCfg cfg; + private URI url; + private int successCode; + private String auth; + + public NativeJiraHttpClient(JiraClientCfg cfg) { + this.cfg = cfg; + + var builder = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(cfg.connectTimeout())); + + if (cfg.httpProtocolVersion() != JiraClientCfg.HttpVersion.DEFAULT) { + if (cfg.httpProtocolVersion() == JiraClientCfg.HttpVersion.HTTP_2) { + builder.version(HttpClient.Version.HTTP_2); + } else if (cfg.httpProtocolVersion() == JiraClientCfg.HttpVersion.HTTP_1_1) { + builder.version(HttpClient.Version.HTTP_1_1); + } + } + + client = builder.build(); + } + + @Override + public JiraHttpClient url(String url) { + this.url = URI.create(url); + return this; + } + + @Override + public JiraHttpClient successCode(int successCode) { + this.successCode = successCode; + return this; + } + + @Override + public JiraHttpClient jiraAuth(String auth) { + this.auth = auth; + return this; + } + + @Override + public Map get() throws IOException { + HttpRequest request = requestBuilder(auth) + .uri(url) + .GET() + .build(); + + return call(request, MAP_TYPE); + } + + @Override + public Map post(Map data) throws IOException { + HttpRequest.BodyPublisher body = HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(data)); + HttpRequest request = requestBuilder(auth) + .uri(url) + .POST(body) + .header(CONTENT_TYPE, "application/json; charset=utf-8") + .build(); + + return call(request, MAP_TYPE); + } + + @Override + public void post(File file) throws IOException { + var requestBody = new MultipartBuilder(Constants.BOUNDARY) + .addFormDataPart("file", file.getName(), new MultipartRequestBodyHandler.PathRequestBody(file.toPath())) + .build(); + + try (InputStream body = requestBody.getContent()) { + var req = requestBuilder(auth) + .uri(url) + .POST(HttpRequest.BodyPublishers.ofInputStream(() -> body)) + .header(CONTENT_TYPE, requestBody.contentType().toString()) + .header("X-Atlassian-Token", "nocheck") + .build(); + + call(req, LIST_OF_MAPS_TYPE); + } + } + + @Override + public void put(Map data) throws IOException { + HttpRequest request = requestBuilder(auth) + .uri(url) + .PUT(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(data))) + .header(CONTENT_TYPE, "application/json; charset=utf-8") + .build(); + + call(request, MAP_TYPE); + } + + @Override + public void delete() throws IOException { + HttpRequest request = requestBuilder(auth) + .uri(url) + .DELETE() + .build(); + + call(request, MAP_TYPE); + } + + private HttpRequest.Builder requestBuilder(String auth) { + return HttpRequest.newBuilder() + .timeout(Duration.ofSeconds(cfg.readTimeout())) + .header("Authorization", auth) + .header("Accept", "application/json"); + } + + T call(HttpRequest request, JavaType returnType) throws IOException { + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + int statusCode = response.statusCode(); + String results = response.body(); + JiraHttpClient.assertResponseCode(statusCode, results, successCode); + + if (results == null || statusCode == 204) { + return null; + } else { + return MAPPER.readValue(results, returnType); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } +} diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/TaskParams.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/TaskParams.java index dcfa163d..7a050cd8 100644 --- a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/TaskParams.java +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/TaskParams.java @@ -23,6 +23,7 @@ import com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables; import com.walmartlabs.concord.runtime.v2.sdk.Variables; +import javax.annotation.Nonnull; import java.util.*; public class TaskParams implements JiraClientCfg { @@ -75,6 +76,7 @@ public static TaskParams of(Variables input, Map defaults) { private static final String JIRA_AUTH_KEY = "auth"; private static final String JIRA_USER_ID_KEY = "userId"; private static final String JIRA_PASSWORD_KEY = "password"; + private static final String JIRA_HTTP_CLIENT_PROTOCOL_VERSION_KEY = "httpClientProtocolVersion"; private static final String CLIENT_CONNECTTIMEOUT = "connectTimeout"; private static final String CLIENT_READTIMEOUT = "readTimeout"; private static final String CLIENT_WRITETIMEOUT = "writeTimeout"; @@ -90,7 +92,7 @@ public Action action() { try { return Action.valueOf(action.trim().toUpperCase()); } catch (IllegalArgumentException e) { - throw new RuntimeException("Unknown action: '" + action + "'. Available actions: " + Arrays.toString(Action.values())); + throw new IllegalArgumentException("Unknown action: '" + action + "'. Available actions: " + Arrays.toString(Action.values())); } } @@ -125,6 +127,13 @@ public String pwd() { return variables.assertString(JIRA_PASSWORD_KEY); } + @Override + public HttpVersion httpProtocolVersion() { + return Optional.ofNullable(variables.getString(JIRA_HTTP_CLIENT_PROTOCOL_VERSION_KEY)) + .map(HttpVersion::from) + .orElse(JiraClientCfg.super.httpProtocolVersion()); + } + public static class CreateIssueParams extends TaskParams { private static final String JIRA_ASSIGNEE_KEY = "assignee"; @@ -178,12 +187,12 @@ public List components() { return variables.getList(JIRA_ISSUE_COMPONENTS_KEY, null); } - public Map customFieldsTypeKv() { - return variables.getMap(JIRA_CUSTOM_FIELDS_KV_KEY, null); + public @Nonnull Map customFieldsTypeKv() { + return variables.getMap(JIRA_CUSTOM_FIELDS_KV_KEY, Map.of()); } - public Map customFieldsTypeAtt() { - return variables.getMap(JIRA_CUSTOM_FIELDS_ATTR_KEY, Collections.emptyMap()); + public @Nonnull Map customFieldsTypeAtt() { + return variables.getMap(JIRA_CUSTOM_FIELDS_ATTR_KEY, Map.of()); } } @@ -283,12 +292,12 @@ public String transitionComment() { return variables.assertString(JIRA_TRANSITION_COMMENT_KEY); } - public Map transitionFieldsTypeKv() { - return variables.getMap(JIRA_CUSTOM_FIELDS_KV_KEY, null); + public @Nonnull Map transitionFieldsTypeKv() { + return variables.getMap(JIRA_CUSTOM_FIELDS_KV_KEY, Map.of()); } - public Map transitionFieldsTypeAtt() { - return variables.getMap(JIRA_CUSTOM_FIELDS_ATTR_KEY, null); + public @Nonnull Map transitionFieldsTypeAtt() { + return variables.getMap(JIRA_CUSTOM_FIELDS_ATTR_KEY, Map.of()); } } @@ -341,7 +350,7 @@ public String issueType() { } @Override - public Map customFieldsTypeAtt() { + public @Nonnull Map customFieldsTypeAtt() { Map customFieldsTypeAtt = new HashMap<>(super.customFieldsTypeAtt()); customFieldsTypeAtt.put("parent", Collections.singletonMap("key", parentKey())); diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/v2/JiraTaskV2.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/v2/JiraTaskV2.java index 70c92835..5fbb3ca0 100644 --- a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/v2/JiraTaskV2.java +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/v2/JiraTaskV2.java @@ -21,6 +21,7 @@ */ import com.walmartlabs.concord.plugins.jira.JiraCredentials; +import com.walmartlabs.concord.plugins.jira.JiraSecretService; import com.walmartlabs.concord.plugins.jira.JiraTaskCommon; import com.walmartlabs.concord.plugins.jira.TaskParams; import com.walmartlabs.concord.runtime.v2.sdk.*; @@ -38,16 +39,32 @@ public class JiraTaskV2 implements Task { @Inject public JiraTaskV2(Context context) { this.context = context; - this.delegate = new JiraTaskCommon((orgName, secretName, password) -> { - SecretService.UsernamePassword up = context.secretService().exportCredentials(orgName, secretName, password); - return new JiraCredentials(up.username(), up.password()); - }); + this.delegate = new JiraTaskCommon(new V2SecretService(context.secretService())); } @Override public TaskResult execute(Variables input) { - Map result = delegate.execute(TaskParams.of(input, context.defaultVariables().toMap())); + Map result = getDelegate().execute(TaskParams.of(input, context.defaultVariables().toMap())); return TaskResult.success() .values(result); } + + JiraTaskCommon getDelegate() { + return delegate; + } + + static class V2SecretService implements JiraSecretService { + private final SecretService secretService; + + public V2SecretService(SecretService secretService) { + this.secretService = secretService; + } + + @Override + public JiraCredentials exportCredentials(String orgName, String secretName, String password) throws Exception { + SecretService.UsernamePassword up = secretService.exportCredentials(orgName, secretName, password); + return new JiraCredentials(up.username(), up.password()); + } + } + } diff --git a/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/AbstractWiremockTest.java b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/AbstractWiremockTest.java new file mode 100644 index 00000000..6aca54e8 --- /dev/null +++ b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/AbstractWiremockTest.java @@ -0,0 +1,264 @@ +package com.walmartlabs.concord.plugins.jira; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2024 Walmart Inc., Concord Authors + * ----- + * 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. + * ===== + */ + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.github.tomakehurst.wiremock.common.ConsoleNotifier; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import com.github.tomakehurst.wiremock.stubbing.ServeEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.Map; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.delete; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.put; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public abstract class AbstractWiremockTest { + + @RegisterExtension + static WireMockExtension rule = WireMockExtension.newInstance() + .options(wireMockConfig() + .dynamicPort() + .notifier(new ConsoleNotifier(true))) + .build(); + + protected static final ObjectMapper objectMapper = new ObjectMapper() + .registerModule(new Jdk8Module()); + + protected JiraClientCfg jiraClientCfg; + + protected void stubForCurrentStatus() { + var body = Map.of( + "fields", Map.of( + "status", Map.of( + "name", "Open" + ) + ) + ); + + var mapper = new ObjectMapper(); + + try { + rule.stubFor(get(urlEqualTo("/issue/issueId?fields=status")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(mapper.writeValueAsString(body))) + ); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Invalid json: " + e.getMessage()); + } + } + + protected void stubForBasicAuth() { + rule.stubFor(post(urlEqualTo("/issue/")) + .willReturn(aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"id\": \"123\",\n" + + " \"key\": \"key1\",\n" + + " \"self\": \"2\"\n" + + "}\n")) + ); + } + + protected void stubForAddAttachment() { + rule.stubFor(post(urlEqualTo("/issue/issueId/attachments")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[{\n" + + " \"id\": \"123\",\n" + + " \"key\": \"key1\",\n" + + " \"self\": \"2\"\n" + + "}]")) + ); + } + + protected void stubForPut() { + rule.stubFor(put(urlEqualTo("/issue/issueId")) + .willReturn(aResponse() + .withStatus(204)) + ); + } + + protected void stubForDelete() { + rule.stubFor(delete(urlEqualTo("/issue/issueId")) + .willReturn(aResponse() + .withStatus(204)) + ); + } + + private static void assertAuth(String value) { + assertEquals("Basic " + Base64.getEncoder().encodeToString("mock-user:mock-pass".getBytes(StandardCharsets.UTF_8)), value); + } + + @BeforeEach + void setUp() { + jiraClientCfg = new JiraClientCfg() { + @Override + public HttpVersion httpProtocolVersion() { + return HttpVersion.HTTP_1_1; + } + }; + } + + abstract JiraHttpClient getClient(JiraClientCfg cfg); + + @Test + void testGet() throws IOException { + stubForCurrentStatus(); + + Map resp = getClient(jiraClientCfg) + .url(rule.baseUrl() + "/issue/issueId?fields=status") + .jiraAuth(new JiraCredentials("mock-user", "mock-pass").authHeaderValue()) + .successCode(200) + .get(); + + assertNotNull(resp); + Map expected = Map.of( + "fields", Map.of( + "status", Map.of( + "name", "Open" + ) + ) + ); + assertEquals(expected, resp); + + ServeEvent event = rule.getAllServeEvents().get(0); + assertNotNull(event); + + assertAuth(event.getRequest().header("Authorization").firstValue()); + } + + @Test + void testPost() throws IOException { + stubForBasicAuth(); + + Map resp = getClient(jiraClientCfg) + .url(rule.baseUrl() + "/issue/") + .jiraAuth(new JiraCredentials("mock-user", "mock-pass").authHeaderValue()) + .successCode(201) + .post(Map.of("field1", "value1")); + + assertNotNull(resp); + assertEquals("123", resp.get("id")); + + ServeEvent event = rule.getAllServeEvents().get(0); + assertNotNull(event); + + Map requestBody = objectMapper.readValue(event.getRequest().getBody(), NativeJiraHttpClient.MAP_TYPE); + assertEquals("value1", requestBody.get("field1")); + + assertAuth(event.getRequest().header("Authorization").firstValue()); + } + + @Test + void testPostFile() throws IOException { + stubForAddAttachment(); + + getClient(jiraClientCfg) + .url(rule.baseUrl() + "/issue/issueId/attachments") + .successCode(200) + .jiraAuth(new JiraCredentials("mock-user", "mock-pass").authHeaderValue()) + .post(Paths.get("src/test/resources/sample.txt").toFile()); + + ServeEvent event = rule.getAllServeEvents().get(0); + assertNotNull(event); + + String requestBody = event.getRequest().getBodyAsString(); + assertTrue(requestBody.contains("Content-Length: 11")); + assertTrue(requestBody.contains("Content-Type: application/octet-stream")); + assertTrue(requestBody.contains("Content-Disposition: form-data; name=\"file\"; filename=\"sample.txt\"")); + + String contentType = event.getRequest().header("Content-Type").firstValue(); + assertEquals("multipart/form-data; boundary=" + Constants.BOUNDARY, contentType); + + assertAuth(event.getRequest().header("Authorization").firstValue()); + } + + @Test + void testPut() throws IOException { + stubForPut(); + + getClient(jiraClientCfg) + .url(rule.baseUrl() + "/issue/issueId") + .successCode(204) + .jiraAuth(new JiraCredentials("mock-user", "mock-pass").authHeaderValue()) + .put(Map.of("aKey", "aValue")); + + ServeEvent event = rule.getAllServeEvents().get(0); + assertNotNull(event); + + assertAuth(event.getRequest().header("Authorization").firstValue()); + } + + @Test + void testDelete() throws IOException { + stubForDelete(); + + getClient(jiraClientCfg) + .url(rule.baseUrl() + "/issue/issueId") + .successCode(204) + .jiraAuth(new JiraCredentials("mock-user", "mock-pass").authHeaderValue()) + .delete(); + + ServeEvent event = rule.getAllServeEvents().get(0); + assertNotNull(event); + + assertAuth(event.getRequest().header("Authorization").firstValue()); + } + + @Test + void testResponseCodes() { + var ex400 = assertThrows(IllegalStateException.class, () -> JiraHttpClient.assertResponseCode(400, "example message", 200)); + assertTrue(ex400.getMessage().contains("input is invalid")); + var ex401 = assertThrows(IllegalStateException.class, () -> JiraHttpClient.assertResponseCode(401, "example message", 200)); + assertTrue(ex401.getMessage().contains("User is not authenticated")); + var ex403 = assertThrows(IllegalStateException.class, () -> JiraHttpClient.assertResponseCode(403, "example message", 200)); + assertTrue(ex403.getMessage().contains("User does not have permission to perform request")); + var ex404 = assertThrows(IllegalStateException.class, () -> JiraHttpClient.assertResponseCode(404, "example message", 200)); + assertTrue(ex404.getMessage().contains("Issue does not exist")); + var ex500 = assertThrows(IllegalStateException.class, () -> JiraHttpClient.assertResponseCode(500, "example message", 200)); + assertTrue(ex500.getMessage().contains("Internal Server Error")); + var exUnknown = assertThrows(IllegalStateException.class, () -> JiraHttpClient.assertResponseCode(501, "example message", 200)); + assertTrue(exUnknown.getMessage().contains("Error: example message")); + } + +} diff --git a/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/CommonTest.java b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/CommonTest.java new file mode 100644 index 00000000..3642a45c --- /dev/null +++ b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/CommonTest.java @@ -0,0 +1,316 @@ +package com.walmartlabs.concord.plugins.jira; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2024 Walmart 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. + * ===== + */ + +import com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CommonTest { + + @Mock + JiraClient jiraClient; + + @Mock + JiraSecretService jiraSecretService; + + @Spy + JiraTaskCommon delegate = new JiraTaskCommon(jiraSecretService); + + Map input; + Map defaults; + + @BeforeEach + public void setup() { + input = new HashMap<>(); + defaults = new HashMap<>(); + defaults.put("apiUrl", "https://localhost:1234/"); + defaults.put( + "auth", Map.of( + "basic", Map.of( + "username", "user", + "password", "pass" + ) + ) + ); + + when(jiraClient.jiraAuth(any())).thenReturn(jiraClient); + when(jiraClient.url(anyString())).thenReturn(jiraClient); + when(jiraClient.successCode(anyInt())).thenReturn(jiraClient); + + doAnswer(invocation -> jiraClient).when(delegate).getClient(any()); + } + + @Test + void testCreateIssueWithBasicAuth() throws Exception { + input.put("action", "createIssue"); + input.put("projectKey", "mock-proj-key"); + input.put("summary", "mock-summary"); + input.put("description", "mock-description"); + input.put("requestorUid", "mock-uid"); + input.put("issueType", "bug"); + + when(jiraClient.post(anyMap())).thenReturn(Map.of("key", "\"result-key\"")); + + var result = delegate.execute(TaskParams.of(new MapBackedVariables(input), defaults)); + assertNotNull(result); + assertEquals("result-key", result.get("issueId")); + + verify(jiraSecretService, times(0)) + .exportCredentials("organization", "secret", null); + + verify(delegate, times(1)).createIssue(any()); + } + + @Test + void testCreateIssueWithSecret() throws Exception { + input.put("action", "createIssue"); + input.put("projectKey", "mock-proj-key"); + input.put("summary", "mock-summary"); + input.put("description", "mock-description"); + input.put("requestorUid", "mock-uid"); + input.put("issueType", "bug"); + input.put("auth", Map.of( + "secret", Map.of( + "org", "organization", + "name", "secret" + ) + )); + + when(jiraSecretService.exportCredentials(any(), any(), any())).thenReturn(new JiraCredentials("user", "pwd")); + when(jiraClient.post(anyMap())).thenReturn(Map.of("key", "\"result-key\"")); + doAnswer(invocation -> jiraSecretService).when(delegate).getSecretService(); + + var result = delegate.execute(TaskParams.of(new MapBackedVariables(input), defaults)); + assertNotNull(result); + assertEquals("result-key", result.get("issueId")); + + verify(jiraSecretService, times(1)) + .exportCredentials("organization", "secret", null); + verify(delegate, times(1)).createIssue(any()); + verify(delegate, times(1)).getSecretService(); + } + + @Test + void testAddComment() throws IOException { + input.put("action", "addComment"); + input.put("issueKey", "mock-issue"); + input.put("comment", "mock-comment"); + + when(jiraClient.post(Mockito.anyMap())).thenReturn(null); + + var result = delegate.execute(TaskParams.of(new MapBackedVariables(input), defaults)); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + verify(delegate, times(1)).addComment(any()); + } + + @Test + void testAddAttachment() throws IOException { + input.put("action", "addAttachment"); + input.put("issueKey", "issueId"); + input.put("userId", "userId"); + input.put("password", "password"); + input.put("filePath", "src/test/resources/sample.txt"); + + doNothing().when(jiraClient).post(Mockito.any(File.class)); + + var result = delegate.execute(TaskParams.of(new MapBackedVariables(input), defaults)); + + assertTrue(result.isEmpty()); + + verify(delegate, times(1)).addAttachment(any()); + } + + @Test + void testCreateComponent() throws IOException { + input.put("action", "createComponent"); + input.put("projectKey", "mock-project"); + input.put("componentName", "mock-component"); + + when(jiraClient.post(Mockito.anyMap())).thenReturn(Map.of("id", "\"321\"")); + + var result = delegate.execute(TaskParams.of(new MapBackedVariables(input), defaults)); + + assertNotNull(result); + assertEquals("321", result.get("componentId")); + + verify(delegate, times(1)).createComponent(any()); + } + + @Test + void testDeleteComponent() throws IOException { + input.put("action", "deleteComponent"); + input.put("componentId", 321); + + doNothing().when(jiraClient).delete(); + + var result = delegate.execute(TaskParams.of(new MapBackedVariables(input), defaults)); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + verify(delegate, times(1)).deleteComponent(any()); + } + + @Test + void testTransition() throws IOException { + input.put("action", "transition"); + input.put("issueKey", "issue-123"); + input.put("transitionId", 543); + input.put("transitionComment", "mock-transition-comment"); + + when(jiraClient.post(Mockito.anyMap())).thenReturn(null); + + var result = delegate.execute(TaskParams.of(new MapBackedVariables(input), defaults)); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + verify(delegate, times(1)).transition(any()); + } + + @Test + void testDeleteIssue() throws IOException { + input.put("action", "deleteIssue"); + input.put("issueKey", "issue-123"); + + doNothing().when(jiraClient).delete(); + + var result = delegate.execute(TaskParams.of(new MapBackedVariables(input), defaults)); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + verify(delegate, times(1)).deleteIssue(any()); + } + + @Test + void testUpdateIssue() throws IOException { + input.put("action", "updateIssue"); + input.put("issueKey", "issue-123"); + input.put("fields", Map.of("field1", "value1", "field2", "value2")); + + doNothing().when(jiraClient).put(anyMap()); + + var result = delegate.execute(TaskParams.of(new MapBackedVariables(input), defaults)); + + assertNotNull(result); + assertTrue(result.isEmpty()); + + verify(delegate, times(1)).updateIssue(any()); + } + + @Test + void testCreateSubTask() throws IOException { + input.put("action", "createSubTask"); + input.put("parentIssueKey", "parent-issue-123"); + input.put("projectKey", "mock-proj-key"); + input.put("summary", "mock-summary"); + input.put("description", "mock-description"); + input.put("requestorUid", "mock-uid"); + input.put("issueType", "bug"); + + when(jiraClient.post(anyMap())).thenReturn(Map.of("key", "\"result-key\"")); + + var result = delegate.execute(TaskParams.of(new MapBackedVariables(input), defaults)); + + assertNotNull(result); + assertEquals("result-key", result.get("issueId")); + + verify(delegate, times(1)).createSubTask(any()); + } + + @Test + void testCurrentStatus() throws IOException { + input.put("action", "currentStatus"); + input.put("issueKey", "issueId"); + + when(jiraClient.get()).thenReturn(Map.of( + "fields", Map.of( + "status", Map.of( + "name", "Open" + ) + ) + )); + + var result = delegate.execute(TaskParams.of(new MapBackedVariables(input), defaults)); + + var status = assertInstanceOf(String.class, result.get("issueStatus")); + assertNotNull(status); + assertEquals("Open", status); + } + + @Test + void testGetIssues() throws IOException { + input.put("action", "getIssues"); + input.put("projectKey", "mock-proj-key"); + input.put("summary", "mock-summary"); + input.put("description", "mock-description"); + input.put("requestorUid", "mock-uid"); + input.put("issueType", "bug"); + + when(jiraClient.post(anyMap())).thenReturn(Map.of( + "issues", List.of( + Map.of( + "id", "123" + ) + ) + )); + + var result = delegate.execute(TaskParams.of(new MapBackedVariables(input), defaults)); + assertNotNull(result); + assertEquals(1, result.get("issueCount")); + + var issues = assertInstanceOf(List.class, result.get("issueList")); + assertEquals(1, issues.size()); + + verify(delegate, times(1)).getIssues(any()); + } +} diff --git a/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/JiraClientTest.java b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/JiraClientTest.java new file mode 100644 index 00000000..1e6056b9 --- /dev/null +++ b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/JiraClientTest.java @@ -0,0 +1,30 @@ +package com.walmartlabs.concord.plugins.jira; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2024 Walmart Inc., Concord Authors + * ----- + * 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. + * ===== + */ + +class JiraClientTest extends AbstractWiremockTest { + + @Override + JiraHttpClient getClient(JiraClientCfg cfg) { + return new JiraClient(cfg); + } + +} diff --git a/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/JiraTaskTest.java b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/JiraTaskTest.java index d0974767..354068da 100644 --- a/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/JiraTaskTest.java +++ b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/JiraTaskTest.java @@ -20,196 +20,94 @@ * ===== */ -import com.github.tomakehurst.wiremock.common.ConsoleNotifier; -import com.github.tomakehurst.wiremock.junit5.WireMockExtension; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonObject; import com.walmartlabs.concord.sdk.Context; +import com.walmartlabs.concord.sdk.MockContext; import com.walmartlabs.concord.sdk.SecretService; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.mockito.ArgumentMatchers; -import org.mockito.Mockito; -import org.mockito.stubbing.Answer; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; +import java.util.UUID; -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.nullable; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -public class JiraTaskTest { +@ExtendWith(MockitoExtension.class) +class JiraTaskTest { - @RegisterExtension - static WireMockExtension rule = WireMockExtension.newInstance() - .options(wireMockConfig() - .dynamicPort() - .notifier(new ConsoleNotifier(true))) - .build(); + @TempDir + Path workDir; - private JiraTask task; - private final Context mockContext = mock(Context.class); - private final SecretService secretService = Mockito.mock(SecretService.class); - protected String response; + @Mock + private SecretService secretService; - @BeforeEach - public void setup() { - task = new JiraTask(secretService); - stubForBasicAuth(); - stubForCurrentStatus(); - stubForAddAttachment(); - } + @Mock + private JiraTaskCommon common; - @AfterEach - public void tearDown() { - response = null; - } + @Spy + private JiraTask task = new JiraTask(secretService); - @Test - public void testCreateIssueWithBasicAuth() throws Exception { - Map auth = new HashMap<>(); - Map basic = new HashMap<>(); - basic.put("username", "user"); - basic.put("password", "pass"); - - auth.put("basic", basic); - String url = rule.baseUrl() + "/"; - initCxtForRequest(mockContext, "CREATEISSUE", url, "projKey", "summary", "description", - "requestorUid", "bug", auth); - - task.execute(mockContext); - } + private Context mockContext; - @Test - public void testCreateIssueWithSecret() throws Exception { - Map auth = new HashMap<>(); - Map secret = new HashMap<>(); - secret.put("name", "secret"); - secret.put("org", "organization"); - - auth.put("secret", secret); - - String url = rule.baseUrl() + "/"; - initCxtForRequest(mockContext, "CREATEISSUE", url, "projKey", "summary", "description", - "requestorUid", "bug", auth); - task.execute(mockContext); - } + @BeforeEach + public void setup() { + mockContext = new MockContext(new HashMap<>()); + mockContext.setVariable("txId", UUID.randomUUID()); + mockContext.setVariable("workDir", workDir.toString()); - @Test - public void testAddAttachment() { - when(mockContext.getVariable("apiUrl")).thenReturn(rule.baseUrl() + "/"); - when(mockContext.getVariable("action")).thenReturn("addAttachment"); - when(mockContext.getVariable("issueKey")).thenReturn("issueId"); - when(mockContext.getVariable("userId")).thenReturn("userId"); - when(mockContext.getVariable("password")).thenReturn("password"); - when(mockContext.getVariable("filePath")).thenReturn("src/test/resources/sample.txt"); - - task.execute(mockContext); } @Test - public void testCurrentStatus() { - when(mockContext.getVariable("action")).thenReturn("currentStatus"); - when(mockContext.getVariable("apiUrl")).thenReturn(rule.baseUrl() + "/"); - when(mockContext.getVariable("issueKey")).thenReturn("issueId"); - when(mockContext.getVariable("userId")).thenReturn("userId"); - when(mockContext.getVariable("password")).thenReturn("password"); - - doAnswer((Answer) invocation -> { - response = (String) invocation.getArguments()[1]; - return null; - }).when(mockContext).setVariable(ArgumentMatchers.anyString(), ArgumentMatchers.any()); - - task.execute(mockContext); - - assertNotNull(response); - assertEquals("Open1", response); - } - - - private void initCxtForRequest(Context ctx, Object action, Object apiUrl, Object projectKey, Object summary, Object description, - Object requestorUid, Object issueType, Object auth) throws Exception { - - when(ctx.getVariable("action")).thenReturn(action); - when(ctx.getVariable("apiUrl")).thenReturn(apiUrl); - when(ctx.getVariable("projectKey")).thenReturn(projectKey); - when(ctx.getVariable("summary")).thenReturn(summary); - when(ctx.getVariable("description")).thenReturn(description); - when(ctx.getVariable("requestorUid")).thenReturn(requestorUid); - when(ctx.getVariable("issueType")).thenReturn(issueType); - when(ctx.getVariable("auth")).thenReturn(auth); + void testExecute() { + mockContext.setVariable("action", "deleteIssue"); - doAnswer((Answer) invocation -> { - response = (String) invocation.getArguments()[1]; - return null; - }).when(ctx).setVariable(anyString(), any()); + when(task.delegate(any())).thenReturn(common); + when(common.execute(any(TaskParams.DeleteIssueParams.class))).thenReturn(Map.of("ok", true)); - doReturn(getCredentials()).when(secretService) - .exportCredentials(any(), anyString(), anyString(), anyString(), anyString(), anyString()); + assertDoesNotThrow(() -> task.execute(mockContext)); + var ok = assertInstanceOf(Boolean.class, mockContext.getVariable("ok")); + assertTrue(ok); } - private Map getCredentials() { - Map credentials = new HashMap<>(); - credentials.put("username", "user"); - credentials.put("password", "pwd"); - return credentials; - } + @Test + void testGetStatus() { + when(task.delegate(any())).thenReturn(common); + when(common.execute(any(TaskParams.CurrentStatusParams.class))).thenReturn(Map.of("issueStatus", "Open")); - private void stubForBasicAuth() { - rule.stubFor(post(urlEqualTo("/issue/")) - .willReturn(aResponse() - .withStatus(201) - .withHeader("Content-Type", "application/json") - //.withHeader("Accept", "application/json") - .withBody("{\n" + - " \"id\": \"123\",\n" + - " \"key\": \"key1\",\n" + - " \"self\": \"2\"\n" + - "}")) - ); - } + var status = assertDoesNotThrow(() -> task.getStatus(mockContext, "issue-123")); - private void stubForAddAttachment() { - rule.stubFor(post(urlEqualTo("/issue/issueId/attachments")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[{\n" + - " \"id\": \"123\",\n" + - " \"key\": \"key1\",\n" + - " \"self\": \"2\"\n" + - "}]")) - ); + assertEquals("Open", status); } - private void stubForCurrentStatus() { - JsonObject status = new JsonObject(); - status.addProperty("name", "Open"); + @Test + void testGetSecretService() throws Exception { + when(secretService.exportCredentials(any(Context.class), anyString(), anyString(), anyString(), anyString(), nullable(String.class))) + .thenReturn(Map.of("username", "foo", "password", "bar")); - JsonObject fields = new JsonObject(); - fields.add("status", status); + var v1SecretService = new JiraTask.V1SecretService(secretService, mockContext); - JsonObject response = new JsonObject(); - response.add("fields", fields); + var creds = v1SecretService.exportCredentials("org", "name", null); - Gson gson = new GsonBuilder() - .setPrettyPrinting() - .create(); + assertEquals("foo", creds.username()); + assertEquals("bar", creds.password()); - rule.stubFor(get(urlEqualTo("/issue/issueId?fields=status")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(gson.toJson(response))) - ); + verify(secretService, times(1)).exportCredentials(any(Context.class), anyString(), anyString(), anyString(), anyString(), nullable(String.class)); } + } diff --git a/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/NativeJiraHttpClientTest.java b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/NativeJiraHttpClientTest.java new file mode 100644 index 00000000..35a11aaa --- /dev/null +++ b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/NativeJiraHttpClientTest.java @@ -0,0 +1,30 @@ +package com.walmartlabs.concord.plugins.jira; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2024 Walmart Inc., Concord Authors + * ----- + * 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. + * ===== + */ + +class NativeJiraHttpClientTest extends AbstractWiremockTest { + + @Override + JiraHttpClient getClient(JiraClientCfg cfg) { + return new NativeJiraHttpClient(cfg); + } + +} diff --git a/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/TaskParamsTest.java b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/TaskParamsTest.java new file mode 100644 index 00000000..0895a025 --- /dev/null +++ b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/TaskParamsTest.java @@ -0,0 +1,63 @@ +package com.walmartlabs.concord.plugins.jira; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2024 Walmart Inc., Concord Authors + * ----- + * 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. + * ===== + */ + +import com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TaskParamsTest { + + @Test + void testHttpVersionDefault() { + Map input = Map.of(); + var params = new TaskParams.DeleteIssueParams(new MapBackedVariables(input)); + + assertEquals(JiraClientCfg.HttpVersion.DEFAULT, params.httpProtocolVersion()); + } + + @Test + void testHttpVersion1() { + Map input = Map.of("httpClientProtocolVersion", "http/1.1"); + var params = new TaskParams.DeleteIssueParams(new MapBackedVariables(input)); + + assertEquals(JiraClientCfg.HttpVersion.HTTP_1_1, params.httpProtocolVersion()); + } + + @Test + void testHttpVersion2() { + Map input = Map.of("httpClientProtocolVersion", "http/2.0"); + var params = new TaskParams.DeleteIssueParams(new MapBackedVariables(input)); + + assertEquals(JiraClientCfg.HttpVersion.HTTP_2, params.httpProtocolVersion()); + } + + @Test + void testHttpVersionUnknown() { + Map input = Map.of("httpClientProtocolVersion", "invalidValue"); + var params = new TaskParams.DeleteIssueParams(new MapBackedVariables(input)); + + assertEquals(JiraClientCfg.HttpVersion.DEFAULT, params.httpProtocolVersion()); + } +} diff --git a/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/v2/JiraTaskV2Test.java b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/v2/JiraTaskV2Test.java new file mode 100644 index 00000000..52129691 --- /dev/null +++ b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/v2/JiraTaskV2Test.java @@ -0,0 +1,102 @@ +package com.walmartlabs.concord.plugins.jira.v2; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2024 Walmart Inc., Concord Authors + * ----- + * 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. + * ===== + */ + +import com.walmartlabs.concord.plugins.jira.JiraTaskCommon; +import com.walmartlabs.concord.plugins.jira.TaskParams; +import com.walmartlabs.concord.runtime.v2.sdk.Context; +import com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables; +import com.walmartlabs.concord.runtime.v2.sdk.SecretService; +import com.walmartlabs.concord.runtime.v2.sdk.TaskResult; +import com.walmartlabs.concord.runtime.v2.sdk.Variables; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class JiraTaskV2Test { + + @Mock + private Context context; + + @Mock + SecretService secretService; + + @Mock + private JiraTaskCommon common; + + private JiraTaskV2 task; + private Map input; + private Variables defaultVariables; + + @BeforeEach + public void setup() { + task = spy(new JiraTaskV2(context)); + input = new HashMap<>(); + defaultVariables = new MapBackedVariables(Map.of()); + } + + @Test + void testExecute() { + input.put("action", "deleteIssue"); + when(task.getDelegate()).thenReturn(common); + when(context.defaultVariables()).thenReturn(defaultVariables); + when(common.execute(any(TaskParams.DeleteIssueParams.class))).thenReturn(Map.of("customResult", "customResultValue")); + + var result = assertDoesNotThrow(() -> task.execute(new MapBackedVariables(input))); + var simpleResult = assertInstanceOf(TaskResult.SimpleResult.class, result); + + assertTrue(simpleResult.ok()); + assertEquals("customResultValue", simpleResult.values().get("customResult")); + } + + @Test + void testSecretService() throws Exception { + when(secretService.exportCredentials(anyString(), anyString(), nullable(String.class))) + .thenReturn(SecretService.UsernamePassword.of("foo", "bar")); + + var v2SecretService = new JiraTaskV2.V2SecretService(secretService); + + var creds = v2SecretService.exportCredentials("org", "name", null); + + assertEquals("foo", creds.username()); + assertEquals("bar", creds.password()); + + verify(secretService, times(1)).exportCredentials(anyString(), anyString(), nullable(String.class)); + } +} From 84dc3623d8f6802a01f4eb9fb3c153af2c7fd245 Mon Sep 17 00:00:00 2001 From: Benjamin Broadaway <4554569+benbroadaway@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:27:10 -0500 Subject: [PATCH 2/3] remove unnecessary client factory. tests up. --- .../concord/plugins/jira/JiraTaskCommon.java | 22 ++++- .../plugins/jira/CommonClientLoaderTest.java | 82 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/CommonClientLoaderTest.java diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraTaskCommon.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraTaskCommon.java index bbc7a687..abb3a78d 100644 --- a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraTaskCommon.java +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraTaskCommon.java @@ -491,7 +491,27 @@ private String configureStatus(String projectKey, String issueType, String issue } JiraHttpClient getClient(TaskParams in) { - return JiraHttpClientFactory.create(in); + try { + return getNativeClient(in); + } catch (NoClassDefFoundError e) { + // client2 may not exist + log.info("Falling back to okhttp client"); + } + + try { + return getOkHttpClient(in); + } catch (Exception | NoClassDefFoundError e) { + // that's very unexpected as long as okhttp is still allowed + throw new IllegalStateException("No jira http client found"); + } + } + + JiraHttpClient getNativeClient(TaskParams in) { + return new NativeJiraHttpClient(in); + } + + JiraHttpClient getOkHttpClient(TaskParams in) { + return new JiraClient(in); } JiraSecretService getSecretService() { diff --git a/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/CommonClientLoaderTest.java b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/CommonClientLoaderTest.java new file mode 100644 index 00000000..c3e146e4 --- /dev/null +++ b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/CommonClientLoaderTest.java @@ -0,0 +1,82 @@ +package com.walmartlabs.concord.plugins.jira; + +/*- + * ***** + * Concord + * ----- + * Copyright (C) 2017 - 2024 Walmart Inc., Concord Authors + * ----- + * 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. + * ===== + */ + +import com.walmartlabs.concord.runtime.v2.sdk.MapBackedVariables; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; + +@ExtendWith(MockitoExtension.class) +class CommonClientLoaderTest { + + @Mock + JiraSecretService jiraSecretService; + + @Spy + JiraTaskCommon delegate = new JiraTaskCommon(jiraSecretService); + + @Test + void testLoadClient() { + Map input = new HashMap<>(); + input.put("action", "getIssues"); + + var client = delegate.getClient(TaskParams.of(new MapBackedVariables(input), Map.of())); + + assertInstanceOf(NativeJiraHttpClient.class, client); + } + + @Test + void testLoadClientFallback() { + Map input = new HashMap<>(); + input.put("action", "getIssues"); + + doThrow(new NoClassDefFoundError()).when(delegate).getNativeClient(any()); + + var client = delegate.getClient(TaskParams.of(new MapBackedVariables(input), Map.of())); + assertInstanceOf(JiraClient.class, client); + } + + @Test + void testNoClient() { + Map input = new HashMap<>(); + input.put("action", "getIssues"); + + doThrow(new NoClassDefFoundError()).when(delegate).getNativeClient(any()); + doThrow(new NoClassDefFoundError()).when(delegate).getOkHttpClient(any()); + + var params = TaskParams.of(new MapBackedVariables(input), Map.of()); + + var expected = assertThrows(IllegalStateException.class, () -> delegate.getClient(params)); + assertTrue(expected.getMessage().contains("No jira http client found")); + } +} From dc62d3435c4faed91b676af855ef33343bfd3db3 Mon Sep 17 00:00:00 2001 From: Benjamin Broadaway <4554569+benbroadaway@users.noreply.github.com> Date: Mon, 24 Jun 2024 12:09:36 -0500 Subject: [PATCH 3/3] http version handling up --- .../concord/plugins/jira/JiraClientCfg.java | 17 +++++++++++------ .../concord/plugins/jira/TaskParamsTest.java | 13 ++++++++++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraClientCfg.java b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraClientCfg.java index 18cf7dfc..46b21bb9 100644 --- a/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraClientCfg.java +++ b/tasks/jira/src/main/java/com/walmartlabs/concord/plugins/jira/JiraClientCfg.java @@ -39,9 +39,9 @@ default HttpVersion httpProtocolVersion() { } enum HttpVersion { - HTTP_1_1("http/1.1"), - HTTP_2("http/2.0"), - DEFAULT("default"); + HTTP_1_1("HTTP/1.1"), + HTTP_2("HTTP/2.0"), + DEFAULT("DEFAULT"); private final String value; @@ -50,14 +50,19 @@ enum HttpVersion { } public static HttpVersion from(String val) { + if (val == null || val.isBlank()) { + return DEFAULT; + } + + var sanitizedVal = val.toUpperCase(); + for (HttpVersion version : HttpVersion.values()) { - if (version.value.equals(val)) { + if (version.value.equals(sanitizedVal)) { return version; } } - return DEFAULT; + throw new IllegalArgumentException("Unsupported HTTP version: " + val); } - } } diff --git a/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/TaskParamsTest.java b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/TaskParamsTest.java index 0895a025..6e4aeeac 100644 --- a/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/TaskParamsTest.java +++ b/tasks/jira/src/test/java/com/walmartlabs/concord/plugins/jira/TaskParamsTest.java @@ -26,6 +26,8 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class TaskParamsTest { @@ -53,11 +55,20 @@ void testHttpVersion2() { assertEquals(JiraClientCfg.HttpVersion.HTTP_2, params.httpProtocolVersion()); } + @Test + void testHttpVersionBlank() { + Map input = Map.of("httpClientProtocolVersion", " "); + var params = new TaskParams.DeleteIssueParams(new MapBackedVariables(input)); + + assertEquals(JiraClientCfg.HttpVersion.DEFAULT, params.httpProtocolVersion()); + } + @Test void testHttpVersionUnknown() { Map input = Map.of("httpClientProtocolVersion", "invalidValue"); var params = new TaskParams.DeleteIssueParams(new MapBackedVariables(input)); - assertEquals(JiraClientCfg.HttpVersion.DEFAULT, params.httpProtocolVersion()); + var expected = assertThrows(IllegalArgumentException.class, params::httpProtocolVersion); + assertTrue(expected.getMessage().contains("Unsupported HTTP version")); } }