This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 064fff3be18e CAMEL-25544: camel-jira - The producers wait for the 
result of their request (#27692)
064fff3be18e is described below

commit 064fff3be18e455af348f73b724e0c463611c760
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Oct 13 01:47:10 2026 +0530

    CAMEL-25544: camel-jira - The producers wait for the result of their 
request (#27692)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../jira/producer/AddCommentProducer.java          |   3 +-
 .../jira/producer/AddIssueLinkProducer.java        |   3 +-
 .../component/jira/producer/AddIssueProducer.java  |   9 +-
 .../jira/producer/AddWorkLogProducer.java          |   4 +-
 .../jira/producer/AttachFileProducer.java          |   6 +-
 .../jira/producer/DeleteIssueProducer.java         |   3 +-
 .../jira/producer/TransitionIssueProducer.java     |   6 +-
 .../component/jira/producer/WatcherProducer.java   |   6 +-
 .../jira/producer/AddCommentProducerTest.java      |   5 +-
 .../jira/producer/AddIssueLinkProducerTest.java    |  10 +-
 .../jira/producer/AddWorkLogProducerTest.java      |   5 +-
 .../AttachFileProducerFromFileRouteTest.java       |   2 +-
 .../AttachFileProducerInputStreamTest.java         |   2 +-
 .../jira/producer/AttachFileProducerTest.java      |   2 +-
 .../producer/DeleteIssueProducerFailureTest.java   | 101 +++++++++
 .../jira/producer/DeleteIssueProducerTest.java     |   2 +-
 .../producer/JiraProducerRejectedRequestTest.java  | 235 +++++++++++++++++++++
 .../jira/producer/TransitionIssueProducerTest.java |   2 +-
 .../jira/producer/WatcherProducerTest.java         |   4 +-
 19 files changed, 380 insertions(+), 30 deletions(-)

diff --git 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddCommentProducer.java
 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddCommentProducer.java
index a1fe4080fb9f..fa27671043c1 100644
--- 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddCommentProducer.java
+++ 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddCommentProducer.java
@@ -54,6 +54,7 @@ public class AddCommentProducer extends DefaultProducer {
         // there is a bug in addComment, it doesn't use the author parameter 
to add the comment
         // it uses the authenticated username. 
https://ecosystem.atlassian.net/browse/JRJC-241
         Comment comment = new Comment(issue.getSelf(), commentStr, null, null, 
now, null, null, null);
-        issueClient.addComment(commentsUri, comment);
+        // wait for the result, so that a request that Jira rejects fails the 
exchange
+        issueClient.addComment(commentsUri, comment).claim();
     }
 }
diff --git 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddIssueLinkProducer.java
 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddIssueLinkProducer.java
index cd27531b4235..152dc230f2c1 100644
--- 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddIssueLinkProducer.java
+++ 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddIssueLinkProducer.java
@@ -60,6 +60,7 @@ public class AddIssueLinkProducer extends DefaultProducer {
         }
 
         LinkIssuesInput linkIssuesInput = new LinkIssuesInput(parentIssueKey, 
childIssueKey, linkType, comment);
-        issueClient.linkIssue(linkIssuesInput);
+        // wait for the result, so that a request that Jira rejects fails the 
exchange
+        issueClient.linkIssue(linkIssuesInput).claim();
     }
 }
diff --git 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddIssueProducer.java
 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddIssueProducer.java
index 9ccd3c9a393f..6f01eb46a5cf 100644
--- 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddIssueProducer.java
+++ 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddIssueProducer.java
@@ -117,18 +117,19 @@ public class AddIssueProducer extends DefaultProducer {
         IssueRestClient issueClient = client.getIssueClient();
         BasicIssue issueCreated = 
issueClient.createIssue(builder.build()).claim();
         Issue issue = issueClient.getIssue(issueCreated.getKey()).claim();
+        // support InOut. The created issue is set before the watchers are 
added, so that it is on the exchange when
+        // adding a watcher fails
+        ExchangeHelper.setInOutBodyPatternAware(exchange, issue);
         if (ObjectHelper.isNotEmpty(watchers)) {
             String[] watArr = watchers.split(",");
             for (String s : watArr) {
                 String watcher = s.trim();
                 if (!watcher.isEmpty()) {
-                    issueClient.addWatcher(issue.getWatchers().getSelf(), 
watcher);
+                    // wait for the result, so that a request that Jira 
rejects fails the exchange
+                    issueClient.addWatcher(issue.getWatchers().getSelf(), 
watcher).claim();
                 }
             }
         }
-
-        // support InOut
-        ExchangeHelper.setInOutBodyPatternAware(exchange, issue);
     }
 
 }
diff --git 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddWorkLogProducer.java
 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddWorkLogProducer.java
index ce687f99ebe3..d501a9957b5d 100644
--- 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddWorkLogProducer.java
+++ 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AddWorkLogProducer.java
@@ -57,7 +57,7 @@ public class AddWorkLogProducer extends DefaultProducer {
 
         WorklogInput worklogInput = WorklogInput.create(issue.getSelf(), 
comment, new DateTime(), minutesSpent);
 
-        issueClient.addWorklog(issue.getWorklogUri(), worklogInput);
-
+        // wait for the result, so that a request that Jira rejects fails the 
exchange
+        issueClient.addWorklog(issue.getWorklogUri(), worklogInput).claim();
     }
 }
diff --git 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AttachFileProducer.java
 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AttachFileProducer.java
index 1bcd2cae4396..7f9d88d208e3 100644
--- 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AttachFileProducer.java
+++ 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/AttachFileProducer.java
@@ -66,10 +66,12 @@ public class AttachFileProducer extends DefaultProducer {
         IssueRestClient issueClient = client.getIssueClient();
         Issue issue = issueClient.getIssue(issueKey).claim();
         URI attachmentsUri = issue.getAttachmentsUri();
+        // wait for the result, so that the body is read before the exchange 
continues (it may be closed or deleted
+        // when the exchange is done), and a request that Jira rejects fails 
the exchange
         if (file != null) {
-            issueClient.addAttachments(attachmentsUri, file);
+            issueClient.addAttachments(attachmentsUri, file).claim();
         } else {
-            issueClient.addAttachment(attachmentsUri, is, name);
+            issueClient.addAttachment(attachmentsUri, is, name).claim();
         }
     }
 }
diff --git 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/DeleteIssueProducer.java
 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/DeleteIssueProducer.java
index a0892a06d7e2..f23e3a92f8b7 100644
--- 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/DeleteIssueProducer.java
+++ 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/DeleteIssueProducer.java
@@ -39,6 +39,7 @@ public class DeleteIssueProducer extends DefaultProducer {
         }
         JiraRestClient client = ((JiraEndpoint) getEndpoint()).getClient();
         IssueRestClient issueClient = client.getIssueClient();
-        issueClient.deleteIssue(issueKey, true);
+        // wait for the result, so that a request that Jira rejects fails the 
exchange
+        issueClient.deleteIssue(issueKey, true).claim();
     }
 }
diff --git 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/TransitionIssueProducer.java
 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/TransitionIssueProducer.java
index 91a0d77af164..d041a40823d6 100644
--- 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/TransitionIssueProducer.java
+++ 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/TransitionIssueProducer.java
@@ -54,13 +54,15 @@ public class TransitionIssueProducer extends 
DefaultProducer {
         IssueRestClient issueClient = client.getIssueClient();
         Issue issue = issueClient.getIssue(issueKey).claim();
         TransitionInput transitionInput = new TransitionInput(transitionId);
-        issueClient.transition(issue, transitionInput);
+        // wait for the results, so that the comment is added after the 
transition, and a request that Jira rejects
+        // fails the exchange
+        issueClient.transition(issue, transitionInput).claim();
         // add a comment in an additional operation as using the 
TransitionInput doesn't add the comment.
         if (commentStr != null) {
             URI commentsUri = issue.getCommentsUri();
             // there is a bug in addComment, it doesn't use the author 
parameter to add the comment
             // it uses the authenticated username. 
https://ecosystem.atlassian.net/browse/JRJC-241
-            issueClient.addComment(commentsUri, Comment.valueOf(commentStr));
+            issueClient.addComment(commentsUri, 
Comment.valueOf(commentStr)).claim();
         }
     }
 }
diff --git 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/WatcherProducer.java
 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/WatcherProducer.java
index f9b869270ad9..2b5f75264615 100644
--- 
a/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/WatcherProducer.java
+++ 
b/components/camel-jira/src/main/java/org/apache/camel/component/jira/producer/WatcherProducer.java
@@ -54,7 +54,8 @@ public class WatcherProducer extends DefaultProducer {
                 for (String s : watArr) {
                     String watcher = s.trim();
                     if (!watcher.isEmpty()) {
-                        issueClient.addWatcher(issue.getWatchers().getSelf(), 
watcher);
+                        // wait for the result, so that a request that Jira 
rejects fails the exchange
+                        issueClient.addWatcher(issue.getWatchers().getSelf(), 
watcher).claim();
                     }
                 }
             }
@@ -63,7 +64,8 @@ public class WatcherProducer extends DefaultProducer {
                 for (String s : watArr) {
                     String watcher = s.trim();
                     if (!watcher.isEmpty()) {
-                        
issueClient.removeWatcher(issue.getWatchers().getSelf(), watcher);
+                        // wait for the result, so that a request that Jira 
rejects fails the exchange
+                        
issueClient.removeWatcher(issue.getWatchers().getSelf(), watcher).claim();
                     }
                 }
             }
diff --git 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AddCommentProducerTest.java
 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AddCommentProducerTest.java
index 79e8c95e6d8c..0b06e98fdd16 100644
--- 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AddCommentProducerTest.java
+++ 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AddCommentProducerTest.java
@@ -25,6 +25,7 @@ import com.atlassian.jira.rest.client.api.JiraRestClient;
 import com.atlassian.jira.rest.client.api.JiraRestClientFactory;
 import com.atlassian.jira.rest.client.api.domain.Comment;
 import com.atlassian.jira.rest.client.api.domain.Issue;
+import io.atlassian.util.concurrent.Promise;
 import io.atlassian.util.concurrent.Promises;
 import org.apache.camel.CamelContext;
 import org.apache.camel.EndpointInject;
@@ -81,14 +82,14 @@ public class AddCommentProducerTest extends 
CamelTestSupport {
         when(jiraRestClientFactory.createWithBasicHttpAuthentication(any(), 
any(), any())).thenReturn(jiraClient);
         when(jiraClient.getIssueClient()).thenReturn(issueRestClient);
 
-        when(issueRestClient.addComment(any(), any())).then((Answer<Void>) inv 
-> {
+        when(issueRestClient.addComment(any(), 
any())).then((Answer<Promise<Void>>) inv -> {
             Collection<Comment> comments = new ArrayList<>();
             for (Comment c : backendIssue.getComments()) {
                 comments.add(c);
             }
             comments.add(newComment(backendIssue.getId(), comments.size() + 1, 
comment));
             backendIssue = createIssueWithComments(backendIssue.getId(), 
comments);
-            return null;
+            return Promises.promise(null);
         });
         backendIssue = createIssueWithComments(1, 1);
         when(issueRestClient.getIssue(any())).then(inv -> 
Promises.promise(backendIssue));
diff --git 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AddIssueLinkProducerTest.java
 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AddIssueLinkProducerTest.java
index 2744e630104e..1b41165d6239 100644
--- 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AddIssueLinkProducerTest.java
+++ 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AddIssueLinkProducerTest.java
@@ -28,6 +28,8 @@ import 
com.atlassian.jira.rest.client.api.JiraRestClientFactory;
 import com.atlassian.jira.rest.client.api.domain.Issue;
 import com.atlassian.jira.rest.client.api.domain.IssueLink;
 import com.atlassian.jira.rest.client.api.domain.input.LinkIssuesInput;
+import io.atlassian.util.concurrent.Promise;
+import io.atlassian.util.concurrent.Promises;
 import org.apache.camel.CamelContext;
 import org.apache.camel.CamelExecutionException;
 import org.apache.camel.EndpointInject;
@@ -128,11 +130,11 @@ public class AddIssueLinkProducerTest extends 
CamelTestSupport {
         headers.put(LINK_TYPE, linkType);
 
         when(issueRestClient.linkIssue(any(LinkIssuesInput.class)))
-                .then((Answer<Void>) inv -> {
+                .then((Answer<Promise<Void>>) inv -> {
                     Collection<IssueLink> links = new ArrayList<>();
                     links.add(newIssueLink(childIssue.getId(), 1, comment));
                     parentIssue = createIssueWithLinks(parentIssue.getId(), 
links);
-                    return null;
+                    return Promises.promise(null);
                 });
 
         template.sendBodyAndHeaders(comment, headers);
@@ -152,11 +154,11 @@ public class AddIssueLinkProducerTest extends 
CamelTestSupport {
         headers.put(LINK_TYPE, linkType);
 
         when(issueRestClient.linkIssue(any(LinkIssuesInput.class)))
-                .then((Answer<Void>) inv -> {
+                .then((Answer<Promise<Void>>) inv -> {
                     Collection<IssueLink> links = new ArrayList<>();
                     links.add(newIssueLink(childIssue.getId(), 1, null));
                     parentIssue = createIssueWithLinks(parentIssue.getId(), 
links);
-                    return null;
+                    return Promises.promise(null);
                 });
 
         template.sendBodyAndHeaders(null, headers);
diff --git 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AddWorkLogProducerTest.java
 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AddWorkLogProducerTest.java
index 0cf40ecaf099..ace025c49f9f 100644
--- 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AddWorkLogProducerTest.java
+++ 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AddWorkLogProducerTest.java
@@ -29,6 +29,7 @@ import 
com.atlassian.jira.rest.client.api.JiraRestClientFactory;
 import com.atlassian.jira.rest.client.api.domain.Issue;
 import com.atlassian.jira.rest.client.api.domain.Worklog;
 import com.atlassian.jira.rest.client.api.domain.input.WorklogInput;
+import io.atlassian.util.concurrent.Promise;
 import io.atlassian.util.concurrent.Promises;
 import org.apache.camel.CamelContext;
 import org.apache.camel.CamelExecutionException;
@@ -127,11 +128,11 @@ public class AddWorkLogProducerTest extends 
CamelTestSupport {
         String comment = "A new test comment " + new Date();
 
         when(issueRestClient.addWorklog(any(URI.class), 
any(WorklogInput.class)))
-                .then((Answer<Void>) inv -> {
+                .then((Answer<Promise<Void>>) inv -> {
                     Collection<Worklog> workLogs = new ArrayList<>();
                     workLogs.add(newWorkLog(backendIssue.getId(), 
minutesSpent, comment));
                     backendIssue = 
createIssueWithWorkLogs(backendIssue.getId(), workLogs);
-                    return null;
+                    return Promises.promise(null);
                 });
 
         template.sendBodyAndHeaders(comment, headers);
diff --git 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AttachFileProducerFromFileRouteTest.java
 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AttachFileProducerFromFileRouteTest.java
index b997b5a1d73a..81184f21f02c 100644
--- 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AttachFileProducerFromFileRouteTest.java
+++ 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AttachFileProducerFromFileRouteTest.java
@@ -106,7 +106,7 @@ public class AttachFileProducerFromFileRouteTest extends 
CamelTestSupport {
             // re-create the issue with the attachment sent by the route
             issue = createIssueWithAttachment(issue.getId(), 
issue.getSummary(), issue.getKey(), issue.getIssueType(),
                     issue.getDescription(), issue.getPriority(), 
issue.getAssignee(), attachments);
-            return null;
+            return Promises.promise(null);
         });
     }
 
diff --git 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AttachFileProducerInputStreamTest.java
 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AttachFileProducerInputStreamTest.java
index 6acc2cfdeb1b..714214e98be0 100644
--- 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AttachFileProducerInputStreamTest.java
+++ 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AttachFileProducerInputStreamTest.java
@@ -102,7 +102,7 @@ public class AttachFileProducerInputStreamTest extends 
CamelTestSupport {
             // re-create the issue with the attachment sent by the route
             issue = createIssueWithAttachment(issue.getId(), 
issue.getSummary(), issue.getKey(), issue.getIssueType(),
                     issue.getDescription(), issue.getPriority(), 
issue.getAssignee(), attachments);
-            return null;
+            return Promises.promise(null);
         });
     }
 
diff --git 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AttachFileProducerTest.java
 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AttachFileProducerTest.java
index 00357bf54a92..5f267a31a68e 100644
--- 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AttachFileProducerTest.java
+++ 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/AttachFileProducerTest.java
@@ -105,7 +105,7 @@ public class AttachFileProducerTest extends 
CamelTestSupport {
             // re-create the issue with the attachment sent by the route
             issue = createIssueWithAttachment(issue.getId(), 
issue.getSummary(), issue.getKey(), issue.getIssueType(),
                     issue.getDescription(), issue.getPriority(), 
issue.getAssignee(), attachments);
-            return null;
+            return Promises.promise(null);
         });
     }
 
diff --git 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/DeleteIssueProducerFailureTest.java
 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/DeleteIssueProducerFailureTest.java
new file mode 100644
index 000000000000..9ee76c545d76
--- /dev/null
+++ 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/DeleteIssueProducerFailureTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.camel.component.jira.producer;
+
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import com.atlassian.jira.rest.client.api.RestClientException;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.camel.component.jira.JiraConstants.ISSUE_KEY;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Uses the real Jira REST client against a local HTTP server: a request that 
Jira rejects must fail the exchange.
+ */
+class DeleteIssueProducerFailureTest extends CamelTestSupport {
+
+    private HttpServer jira;
+    private volatile int status;
+    private final List<String> requests = new CopyOnWriteArrayList<>();
+
+    @Override
+    protected void setupResources() throws Exception {
+        jira = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+        jira.createContext("/", exchange -> {
+            requests.add(exchange.getRequestMethod() + " " + 
exchange.getRequestURI().getPath());
+            byte[] body = status == 204
+                    ? new byte[0]
+                    : "{\"errorMessages\":[\"You do not have permission to 
delete issues in this project.\"],\"errors\":{}}"
+                            .getBytes(StandardCharsets.UTF_8);
+            exchange.getResponseHeaders().add("Content-Type", 
"application/json");
+            exchange.sendResponseHeaders(status, body.length == 0 ? -1 : 
body.length);
+            if (body.length > 0) {
+                exchange.getResponseBody().write(body);
+            }
+            exchange.close();
+        });
+        jira.start();
+    }
+
+    @Override
+    protected void cleanupResources() {
+        jira.stop(0);
+    }
+
+    @Test
+    void failsTheExchangeWhenJiraRejectsTheRequest() {
+        status = 403;
+
+        Exchange exchange = template.request("direct:start", e -> 
e.getIn().setHeader(ISSUE_KEY, "TST-1"));
+
+        assertEquals(List.of("DELETE /rest/api/latest/issue/TST-1"), requests);
+        RestClientException e = assertInstanceOf(RestClientException.class, 
exchange.getException());
+        assertEquals(403, e.getStatusCode().get());
+    }
+
+    @Test
+    void completesTheExchangeWhenJiraDeletedTheIssue() {
+        status = 204;
+
+        Exchange exchange = template.request("direct:start", e -> 
e.getIn().setHeader(ISSUE_KEY, "TST-1"));
+
+        assertEquals(List.of("DELETE /rest/api/latest/issue/TST-1"), requests);
+        assertNull(exchange.getException());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start")
+                        .to("jira:deleteIssue?jiraUrl=http://localhost:"; + 
jira.getAddress().getPort()
+                            + "&username=user&password=secret");
+            }
+        };
+    }
+}
diff --git 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/DeleteIssueProducerTest.java
 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/DeleteIssueProducerTest.java
index c4b5c3688322..7ac3d20f4aa0 100644
--- 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/DeleteIssueProducerTest.java
+++ 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/DeleteIssueProducerTest.java
@@ -81,7 +81,7 @@ public class DeleteIssueProducerTest extends CamelTestSupport 
{
         when(issueRestClient.getIssue(any())).then(inv -> 
Promises.promise(issue));
         when(issueRestClient.deleteIssue(anyString(), anyBoolean())).then(inv 
-> {
             issue = null;
-            return null;
+            return Promises.promise(null);
         });
     }
 
diff --git 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/JiraProducerRejectedRequestTest.java
 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/JiraProducerRejectedRequestTest.java
new file mode 100644
index 000000000000..05069bf73e87
--- /dev/null
+++ 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/JiraProducerRejectedRequestTest.java
@@ -0,0 +1,235 @@
+/*
+ * 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.camel.component.jira.producer;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.atlassian.jira.rest.client.api.IssueRestClient;
+import com.atlassian.jira.rest.client.api.JiraRestClient;
+import com.atlassian.jira.rest.client.api.JiraRestClientFactory;
+import com.atlassian.jira.rest.client.api.RestClientException;
+import com.atlassian.jira.rest.client.api.domain.BasicIssue;
+import com.atlassian.jira.rest.client.api.domain.BasicWatchers;
+import com.atlassian.jira.rest.client.api.domain.Comment;
+import com.atlassian.jira.rest.client.api.domain.Issue;
+import com.atlassian.jira.rest.client.api.domain.input.IssueInput;
+import com.atlassian.jira.rest.client.api.domain.input.LinkIssuesInput;
+import com.atlassian.jira.rest.client.api.domain.input.TransitionInput;
+import com.atlassian.jira.rest.client.api.domain.input.WorklogInput;
+import io.atlassian.util.concurrent.Promise;
+import io.atlassian.util.concurrent.Promises;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.jira.JiraComponent;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.apache.camel.component.jira.JiraConstants.CHILD_ISSUE_KEY;
+import static org.apache.camel.component.jira.JiraConstants.ISSUE_KEY;
+import static org.apache.camel.component.jira.JiraConstants.ISSUE_PROJECT_KEY;
+import static org.apache.camel.component.jira.JiraConstants.ISSUE_SUMMARY;
+import static 
org.apache.camel.component.jira.JiraConstants.ISSUE_TRANSITION_ID;
+import static org.apache.camel.component.jira.JiraConstants.ISSUE_TYPE_ID;
+import static org.apache.camel.component.jira.JiraConstants.ISSUE_WATCHERS_ADD;
+import static 
org.apache.camel.component.jira.JiraConstants.ISSUE_WATCHERS_REMOVE;
+import static org.apache.camel.component.jira.JiraConstants.JIRA;
+import static 
org.apache.camel.component.jira.JiraConstants.JIRA_REST_CLIENT_FACTORY;
+import static org.apache.camel.component.jira.JiraConstants.LINK_TYPE;
+import static org.apache.camel.component.jira.JiraConstants.MINUTES_SPENT;
+import static org.apache.camel.component.jira.JiraConstants.PARENT_ISSUE_KEY;
+import static 
org.apache.camel.component.jira.JiraTestConstants.JIRA_CREDENTIALS;
+import static org.apache.camel.component.jira.JiraTestConstants.KEY;
+import static org.apache.camel.component.jira.JiraTestConstants.TEST_JIRA_URL;
+import static org.apache.camel.component.jira.Utils.createIssue;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.when;
+
+/**
+ * The Jira client is asynchronous: a request that Jira rejects (no 
permission, an invalid transition, a missing field)
+ * fails its promise. The producers must wait for the promise, so that the 
exchange fails too.
+ */
+@ExtendWith(MockitoExtension.class)
+class JiraProducerRejectedRequestTest extends CamelTestSupport {
+
+    private static final String ISSUE = KEY + "-11";
+
+    private final RestClientException rejection
+            = new RestClientException("Jira rejected the request", new 
IllegalStateException("HTTP 400"));
+
+    private Issue issue;
+
+    @Mock
+    private JiraRestClient jiraClient;
+    @Mock
+    private JiraRestClientFactory jiraRestClientFactory;
+    @Mock
+    private IssueRestClient issueRestClient;
+
+    @Override
+    protected void bindToRegistry(Registry registry) {
+        registry.bind(JIRA_REST_CLIENT_FACTORY, jiraRestClientFactory);
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        URI watchersUri = URI.create(TEST_JIRA_URL + "/rest/api/2/issue/" + 
ISSUE + "/watchers");
+        issue = createIssue(11L, "Test issue", ISSUE, null, null, null, null, 
null,
+                new BasicWatchers(watchersUri, true, 0));
+        Promise<Void> rejected = Promises.rejected(rejection);
+
+        
lenient().when(jiraRestClientFactory.createWithBasicHttpAuthentication(any(), 
any(), any())).thenReturn(jiraClient);
+        
lenient().when(jiraClient.getIssueClient()).thenReturn(issueRestClient);
+        // reading and creating an issue work
+        
lenient().when(issueRestClient.getIssue(anyString())).thenReturn(Promises.promise(issue));
+        lenient().when(issueRestClient.createIssue(any(IssueInput.class)))
+                .thenReturn(Promises.promise(new BasicIssue(issue.getSelf(), 
ISSUE, 11L)));
+        // every request that changes the issue is rejected
+        lenient().when(issueRestClient.addComment(any(), 
any(Comment.class))).thenReturn(rejected);
+        
lenient().when(issueRestClient.linkIssue(any(LinkIssuesInput.class))).thenReturn(rejected);
+        lenient().when(issueRestClient.addWorklog(any(), 
any(WorklogInput.class))).thenReturn(rejected);
+        lenient().when(issueRestClient.addAttachments(any(), 
any(File.class))).thenReturn(rejected);
+        lenient().when(issueRestClient.addAttachment(any(), 
any(InputStream.class), anyString())).thenReturn(rejected);
+        lenient().when(issueRestClient.transition(any(Issue.class), 
any(TransitionInput.class))).thenReturn(rejected);
+        lenient().when(issueRestClient.addWatcher(any(), 
anyString())).thenReturn(rejected);
+        lenient().when(issueRestClient.removeWatcher(any(), 
anyString())).thenReturn(rejected);
+
+        CamelContext camelContext = super.createCamelContext();
+        camelContext.disableJMX();
+        camelContext.addComponent(JIRA, new JiraComponent(camelContext));
+        return camelContext;
+    }
+
+    @Test
+    void addCommentFails() {
+        assertRejected("addComment", "A comment", Map.of(ISSUE_KEY, ISSUE));
+    }
+
+    @Test
+    void addIssueLinkFails() {
+        assertRejected("addIssueLink", null,
+                Map.of(PARENT_ISSUE_KEY, ISSUE, CHILD_ISSUE_KEY, KEY + "-12", 
LINK_TYPE, "Relates"));
+    }
+
+    @Test
+    void addWorkLogFails() {
+        assertRejected("addWorkLog", "A comment", Map.of(ISSUE_KEY, ISSUE, 
MINUTES_SPENT, 10));
+    }
+
+    @Test
+    void attachAFileFails(@TempDir Path dir) throws Exception {
+        File file = Files.writeString(dir.resolve("attachment.txt"), 
"content").toFile();
+
+        assertRejected("attach", file, Map.of(ISSUE_KEY, ISSUE));
+    }
+
+    @Test
+    void attachAStreamFails() {
+        InputStream body = new 
ByteArrayInputStream("content".getBytes(StandardCharsets.UTF_8));
+
+        assertRejected("attach", body, Map.of(ISSUE_KEY, ISSUE, 
Exchange.FILE_NAME, "attachment.txt"));
+    }
+
+    @Test
+    void transitionIssueFails() {
+        assertRejected("transitionIssue", null, Map.of(ISSUE_KEY, ISSUE, 
ISSUE_TRANSITION_ID, 3));
+    }
+
+    @Test
+    void transitionIssueFailsWhenItsCommentIsRejected() {
+        // partial failure: Jira makes the transition and then rejects the 
comment (the message body)
+        when(issueRestClient.transition(any(Issue.class), 
any(TransitionInput.class))).thenReturn(Promises.promise(null));
+
+        assertRejected("transitionIssue", "A comment", Map.of(ISSUE_KEY, 
ISSUE, ISSUE_TRANSITION_ID, 3));
+
+        // the transition was requested before the comment
+        InOrder inOrder = inOrder(issueRestClient);
+        inOrder.verify(issueRestClient).transition(same(issue), 
any(TransitionInput.class));
+        inOrder.verify(issueRestClient).addComment(any(), any(Comment.class));
+    }
+
+    @Test
+    void addWatcherFails() {
+        assertRejected("watchers", null, Map.of(ISSUE_KEY, ISSUE, 
ISSUE_WATCHERS_ADD, "someone"));
+    }
+
+    @Test
+    void removeWatcherFails() {
+        assertRejected("watchers", null, Map.of(ISSUE_KEY, ISSUE, 
ISSUE_WATCHERS_REMOVE, "someone"));
+    }
+
+    @Test
+    void addIssueWithAWatcherFails() {
+        assertRejected("addIssue", "A description",
+                Map.of(ISSUE_PROJECT_KEY, KEY, ISSUE_TYPE_ID, 1L, 
ISSUE_SUMMARY, "A summary", ISSUE_WATCHERS_ADD, "someone"));
+    }
+
+    @Test
+    void addIssueKeepsTheCreatedIssueWhenAWatcherFails() {
+        Exchange exchange = template.request("direct:addIssue", e -> {
+            e.getIn().setBody("A description");
+            e.getIn().setHeaders(new HashMap<>(
+                    Map.of(ISSUE_PROJECT_KEY, KEY, ISSUE_TYPE_ID, 1L, 
ISSUE_SUMMARY, "A summary", ISSUE_WATCHERS_ADD,
+                            "someone")));
+        });
+
+        assertSame(rejection, exchange.getException());
+        // the issue exists in Jira, so the exchange must carry it for the 
error handling of the route
+        assertSame(issue, exchange.getMessage().getBody());
+    }
+
+    private void assertRejected(String type, Object body, Map<String, Object> 
headers) {
+        Exchange exchange = template.request("direct:" + type, e -> {
+            e.getIn().setBody(body);
+            e.getIn().setHeaders(new HashMap<>(headers));
+        });
+
+        assertSame(rejection, exchange.getException());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                for (String type : new String[] {
+                        "addComment", "addIssueLink", "addWorkLog", "attach", 
"transitionIssue", "watchers", "addIssue" }) {
+                    from("direct:" + type).to("jira://" + type + "?jiraUrl=" + 
JIRA_CREDENTIALS);
+                }
+            }
+        };
+    }
+}
diff --git 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/TransitionIssueProducerTest.java
 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/TransitionIssueProducerTest.java
index 12ae454fc6ae..a399301f4345 100644
--- 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/TransitionIssueProducerTest.java
+++ 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/TransitionIssueProducerTest.java
@@ -90,7 +90,7 @@ public class TransitionIssueProducerTest extends 
CamelTestSupport {
             Status status = new Status(doneStatusUri, 1L, "Done", "Done", 
null, null);
             Resolution resolution = new Resolution(doneResolutionUri, 31L, 
"Resolution", "Resolution");
             issue = transitionIssueDone(issue, status, resolution);
-            return null;
+            return Promises.promise(null);
         });
     }
 
diff --git 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/WatcherProducerTest.java
 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/WatcherProducerTest.java
index 19a6b26131df..77b718a2a523 100644
--- 
a/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/WatcherProducerTest.java
+++ 
b/components/camel-jira/src/test/java/org/apache/camel/component/jira/producer/WatcherProducerTest.java
@@ -106,7 +106,7 @@ public class WatcherProducerTest extends CamelTestSupport {
             backendIssue = createIssue(backendIssue.getId(), 
backendIssue.getSummary(), backendIssue.getKey(),
                     backendIssue.getIssueType(), backendIssue.getDescription(),
                     backendIssue.getPriority(), backendIssue.getAssignee(), 
null, basicWatchers);
-            return null;
+            return Promises.promise(null);
         });
         lenient().when(issueRestClient.removeWatcher(any(URI.class), 
anyString())).then(inv -> {
             String username = inv.getArgument(1);
@@ -115,7 +115,7 @@ public class WatcherProducerTest extends CamelTestSupport {
             backendIssue = createIssue(backendIssue.getId(), 
backendIssue.getSummary(), backendIssue.getKey(),
                     backendIssue.getIssueType(), backendIssue.getDescription(),
                     backendIssue.getPriority(), backendIssue.getAssignee(), 
null, basicWatchers);
-            return null;
+            return Promises.promise(null);
         });
         lenient().when(issueRestClient.getIssue(anyString())).then(inv -> 
Promises.promise(backendIssue));
         lenient().when(issueRestClient.getWatchers(any(URI.class))).then(inv 
-> {

Reply via email to