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

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

commit 5b2016dfae5ae07a13a396d033aa0ca54714bc2b
Author: Christoph Deppisch <[email protected]>
AuthorDate: Wed Oct 14 20:04:34 2026 +0200

    CAMEL-25378: Address review feedback on camel-test-citrus
    
    - Validate operation errors: record failures instead of throwing,
      reported via assertIsSatisfied() (MockEndpoint pattern)
    - Receive operation: still throws on validation failure
    - Add operation validation in CitrusComponent (reject invalid ops)
    - Add volatile to shared mutable fields for thread safety
    - Reset counters in expectedMessageCount()
    - Auto-derive expected count from indexed specs
    - Add CitrusContext lifecycle management (doStop)
    - Propagate exchange variables (both directions) between Camel/Citrus
    - Use Locale.ROOT for case conversions
    - Switch test dependency to camel-test-junit6
    - Update firstVersion to 4.24.0
    - Add operation enum constraint
    - Improve documentation: Maven dependency snippet, path validation
      API usage, missing spec error behavior, exchange variables
    - Regenerate catalog and endpoint DSL artifacts
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../apache/camel/catalog/components/citrus.json    |  4 +-
 .../camel/catalog/docs/citrus-component.adoc       | 47 ++++++++++++++++++----
 components/camel-test/camel-test-citrus/pom.xml    | 10 +----
 .../org/apache/camel/component/citrus/citrus.json  |  4 +-
 .../src/main/docs/citrus-component.adoc            | 47 ++++++++++++++++++----
 .../camel/component/citrus/CitrusComponent.java    |  4 ++
 .../camel/component/citrus/CitrusEndpoint.java     | 28 ++++++++-----
 .../camel/component/citrus/CitrusProducer.java     | 46 ++++++++++++++++-----
 .../citrus/CitrusMultipleEndpointsTest.java        |  2 +-
 .../camel/component/citrus/CitrusReceiveTest.java  |  2 +-
 .../citrus/CitrusSequentialValidationTest.java     | 22 +++++-----
 .../component/citrus/CitrusValidateJsonTest.java   | 12 ++++--
 .../citrus/CitrusValidatePlainTextTest.java        | 11 +++--
 .../component/citrus/CitrusValidateXmlTest.java    |  2 +-
 .../component/citrus/CitrusValidateYamlTest.java   | 13 +++---
 .../builder/endpoint/StaticEndpointBuilders.java   |  6 ++-
 .../endpoint/dsl/CitrusEndpointBuilderFactory.java |  6 ++-
 17 files changed, 189 insertions(+), 77 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/citrus.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/citrus.json
index 72f7bf646491..ced95b3a80fb 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/citrus.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/citrus.json
@@ -5,7 +5,7 @@
     "title": "Citrus",
     "description": "Citrus message validation component for testing",
     "deprecated": false,
-    "firstVersion": "4.23.0",
+    "firstVersion": "4.24.0",
     "label": "testing",
     "javaType": "org.apache.camel.component.citrus.CitrusComponent",
     "supportLevel": "Preview",
@@ -28,7 +28,7 @@
     "autowiredEnabled": { "index": 1, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching t [...]
   },
   "properties": {
-    "operation": { "index": 0, "kind": "path", "displayName": "Operation", 
"group": "producer", "label": "", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The operation to perform. 
'validate' validates exchanges without modifying them. 'receive' validates 
exchanges and returns a simulated response." },
+    "operation": { "index": 0, "kind": "path", "displayName": "Operation", 
"group": "producer", "label": "", "required": true, "type": "enum", "javaType": 
"java.lang.String", "enum": [ "validate", "receive" ], "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": "The 
operation to perform. 'validate' validates exchanges without modifying them. 
'receive' validates exchanges and returns a simulated response." },
     "name": { "index": 1, "kind": "path", "displayName": "Name", "group": 
"producer", "label": "", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"description": "A unique name to identify this endpoint. Allows multiple Citrus 
endpoints with different validation expectations in the same route." },
     "messageType": { "index": 2, "kind": "parameter", "displayName": "Message 
Type", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "The message type for validation. Determines 
which Citrus validation module is used. Supported values: json, xml, yaml, 
plaintext." },
     "lazyStartProducer": { "index": 3, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produc [...]
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/citrus-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/citrus-component.adoc
index 2a385b38d6be..280ee52e74a4 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/citrus-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/citrus-component.adoc
@@ -3,7 +3,7 @@
 :shortname: citrus
 :artifactid: camel-test-citrus
 :description: Citrus message validation component for testing
-:since: 4.23
+:since: 4.24
 :supportlevel: Preview
 :tabs-sync-option:
 :component-header: Only producer is supported
@@ -19,6 +19,17 @@ modules.
 
 This component is intended for use in Camel route unit and integration tests.
 
+== Maven dependency
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.camel</groupId>
+    <artifactId>camel-test-citrus</artifactId>
+    <scope>test</scope>
+</dependency>
+----
+
 == URI format
 
 ----
@@ -257,7 +268,9 @@ it replaces `allMessages()` for that exchange entirely (the 
two are never combin
 `allMessages()` spec acts as a fallback for exchanges that have no matching 
indexed spec.
 
 If an exchange arrives at an index with no matching indexed spec and no 
`allMessages()` fallback,
-the endpoint fails with an error.
+the endpoint records a failure. For the `validate` operation, the failure is 
collected and reported
+when `assertIsSatisfied()` is called (similar to MockEndpoint). For the 
`receive` operation, the
+failure is thrown immediately as an exception.
 
 [source,java]
 ----
@@ -365,14 +378,32 @@ citrus.then().allMessages().respond()
 citrus.expectedMessageCount(1);
 ----
 
-=== Exchange properties as Citrus variables
+=== Exchange properties and variables as Citrus variables
 
-All exchange properties are automatically propagated to the Citrus test 
context as test variables.
-This enables the use of Citrus functions and validation matchers in expected 
message templates.
+All exchange properties and exchange variables are automatically propagated to 
the Citrus test
+context as test variables. This enables the use of Citrus variable 
placeholders (`${variableName}`)
+in expected message templates and response bodies.
+
+Variables extracted by Citrus during validation (e.g., via 
`@variable('name')@`) are written back
+to the exchange as exchange variables, making them available for downstream 
route processing.
 
 === Path validation
 
-Path validation expressions are automatically classified:
+Use the `validate()` method on a `MessageValidationSpec` to add Citrus 
validation context builders
+such as JsonPath or XPath expressions. These are provided by the Citrus 
`DefaultValidations` class:
 
-* Expressions starting with `$` are treated as JsonPath
-* Expressions starting with `/` are treated as XPath
+[source,java]
+----
+CitrusEndpoint citrus = 
context.getEndpoint("citrus:validate?messageType=json", CitrusEndpoint.class);
+citrus.allMessages().validate(new DefaultValidations().jsonPath()
+    .expression("$.user.name", "John")
+    .expression("$.user.age", "30"));
+----
+
+[source,java]
+----
+CitrusEndpoint citrus = context.getEndpoint("citrus:validate?messageType=xml", 
CitrusEndpoint.class);
+citrus.allMessages().validate(new DefaultValidations().xpath()
+    .expression("/order/status", "completed")
+    .expression("/order/total", "99.99"));
+----
diff --git a/components/camel-test/camel-test-citrus/pom.xml 
b/components/camel-test/camel-test-citrus/pom.xml
index 7ce080a3ff84..58ca14a9c68b 100644
--- a/components/camel-test/camel-test-citrus/pom.xml
+++ b/components/camel-test/camel-test-citrus/pom.xml
@@ -33,7 +33,7 @@
     <description>Citrus message validation component for testing</description>
 
     <properties>
-        <firstVersion>4.23.0</firstVersion>
+        <firstVersion>4.24.0</firstVersion>
         <label>testing,java</label>
         <title>Test Citrus</title>
     </properties>
@@ -59,7 +59,7 @@
         <!-- Test dependencies -->
         <dependency>
             <groupId>org.apache.camel</groupId>
-            <artifactId>camel-test-junit5</artifactId>
+            <artifactId>camel-test-junit6</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -99,15 +99,9 @@
             <scope>test</scope>
         </dependency>
 
-        <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter</artifactId>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
-            <version>${assertj-version}</version>
             <scope>test</scope>
         </dependency>
         <dependency>
diff --git 
a/components/camel-test/camel-test-citrus/src/generated/resources/META-INF/org/apache/camel/component/citrus/citrus.json
 
b/components/camel-test/camel-test-citrus/src/generated/resources/META-INF/org/apache/camel/component/citrus/citrus.json
index 72f7bf646491..ced95b3a80fb 100644
--- 
a/components/camel-test/camel-test-citrus/src/generated/resources/META-INF/org/apache/camel/component/citrus/citrus.json
+++ 
b/components/camel-test/camel-test-citrus/src/generated/resources/META-INF/org/apache/camel/component/citrus/citrus.json
@@ -5,7 +5,7 @@
     "title": "Citrus",
     "description": "Citrus message validation component for testing",
     "deprecated": false,
-    "firstVersion": "4.23.0",
+    "firstVersion": "4.24.0",
     "label": "testing",
     "javaType": "org.apache.camel.component.citrus.CitrusComponent",
     "supportLevel": "Preview",
@@ -28,7 +28,7 @@
     "autowiredEnabled": { "index": 1, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching t [...]
   },
   "properties": {
-    "operation": { "index": 0, "kind": "path", "displayName": "Operation", 
"group": "producer", "label": "", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The operation to perform. 
'validate' validates exchanges without modifying them. 'receive' validates 
exchanges and returns a simulated response." },
+    "operation": { "index": 0, "kind": "path", "displayName": "Operation", 
"group": "producer", "label": "", "required": true, "type": "enum", "javaType": 
"java.lang.String", "enum": [ "validate", "receive" ], "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": "The 
operation to perform. 'validate' validates exchanges without modifying them. 
'receive' validates exchanges and returns a simulated response." },
     "name": { "index": 1, "kind": "path", "displayName": "Name", "group": 
"producer", "label": "", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "autowired": false, "secret": false, 
"description": "A unique name to identify this endpoint. Allows multiple Citrus 
endpoints with different validation expectations in the same route." },
     "messageType": { "index": 2, "kind": "parameter", "displayName": "Message 
Type", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "The message type for validation. Determines 
which Citrus validation module is used. Supported values: json, xml, yaml, 
plaintext." },
     "lazyStartProducer": { "index": 3, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produc [...]
diff --git 
a/components/camel-test/camel-test-citrus/src/main/docs/citrus-component.adoc 
b/components/camel-test/camel-test-citrus/src/main/docs/citrus-component.adoc
index 2a385b38d6be..280ee52e74a4 100644
--- 
a/components/camel-test/camel-test-citrus/src/main/docs/citrus-component.adoc
+++ 
b/components/camel-test/camel-test-citrus/src/main/docs/citrus-component.adoc
@@ -3,7 +3,7 @@
 :shortname: citrus
 :artifactid: camel-test-citrus
 :description: Citrus message validation component for testing
-:since: 4.23
+:since: 4.24
 :supportlevel: Preview
 :tabs-sync-option:
 :component-header: Only producer is supported
@@ -19,6 +19,17 @@ modules.
 
 This component is intended for use in Camel route unit and integration tests.
 
+== Maven dependency
+
+[source,xml]
+----
+<dependency>
+    <groupId>org.apache.camel</groupId>
+    <artifactId>camel-test-citrus</artifactId>
+    <scope>test</scope>
+</dependency>
+----
+
 == URI format
 
 ----
@@ -257,7 +268,9 @@ it replaces `allMessages()` for that exchange entirely (the 
two are never combin
 `allMessages()` spec acts as a fallback for exchanges that have no matching 
indexed spec.
 
 If an exchange arrives at an index with no matching indexed spec and no 
`allMessages()` fallback,
-the endpoint fails with an error.
+the endpoint records a failure. For the `validate` operation, the failure is 
collected and reported
+when `assertIsSatisfied()` is called (similar to MockEndpoint). For the 
`receive` operation, the
+failure is thrown immediately as an exception.
 
 [source,java]
 ----
@@ -365,14 +378,32 @@ citrus.then().allMessages().respond()
 citrus.expectedMessageCount(1);
 ----
 
-=== Exchange properties as Citrus variables
+=== Exchange properties and variables as Citrus variables
 
-All exchange properties are automatically propagated to the Citrus test 
context as test variables.
-This enables the use of Citrus functions and validation matchers in expected 
message templates.
+All exchange properties and exchange variables are automatically propagated to 
the Citrus test
+context as test variables. This enables the use of Citrus variable 
placeholders (`${variableName}`)
+in expected message templates and response bodies.
+
+Variables extracted by Citrus during validation (e.g., via 
`@variable('name')@`) are written back
+to the exchange as exchange variables, making them available for downstream 
route processing.
 
 === Path validation
 
-Path validation expressions are automatically classified:
+Use the `validate()` method on a `MessageValidationSpec` to add Citrus 
validation context builders
+such as JsonPath or XPath expressions. These are provided by the Citrus 
`DefaultValidations` class:
 
-* Expressions starting with `$` are treated as JsonPath
-* Expressions starting with `/` are treated as XPath
+[source,java]
+----
+CitrusEndpoint citrus = 
context.getEndpoint("citrus:validate?messageType=json", CitrusEndpoint.class);
+citrus.allMessages().validate(new DefaultValidations().jsonPath()
+    .expression("$.user.name", "John")
+    .expression("$.user.age", "30"));
+----
+
+[source,java]
+----
+CitrusEndpoint citrus = context.getEndpoint("citrus:validate?messageType=xml", 
CitrusEndpoint.class);
+citrus.allMessages().validate(new DefaultValidations().xpath()
+    .expression("/order/status", "completed")
+    .expression("/order/total", "99.99"));
+----
diff --git 
a/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusComponent.java
 
b/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusComponent.java
index 46930c00acad..57e3d47e1075 100644
--- 
a/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusComponent.java
+++ 
b/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusComponent.java
@@ -39,6 +39,10 @@ public class CitrusComponent extends DefaultComponent {
             operation = remaining;
         }
 
+        if (!"validate".equals(operation) && !"receive".equals(operation)) {
+            throw new IllegalArgumentException(
+                    "Invalid citrus operation: '" + operation + "'. Valid 
values are: validate, receive");
+        }
         endpoint.setOperation(operation);
         endpoint.setName(name);
         setProperties(endpoint, parameters);
diff --git 
a/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusEndpoint.java
 
b/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusEndpoint.java
index db8361e85780..b5d5cf59224d 100644
--- 
a/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusEndpoint.java
+++ 
b/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusEndpoint.java
@@ -39,7 +39,7 @@ import org.citrusframework.exceptions.ValidationException;
 import org.citrusframework.message.MessagePayloadBuilder;
 import org.citrusframework.validation.context.ValidationContext;
 
-@UriEndpoint(firstVersion = "4.23.0", scheme = "citrus", title = "Citrus", 
syntax = "citrus:operation/name",
+@UriEndpoint(firstVersion = "4.24.0", scheme = "citrus", title = "Citrus", 
syntax = "citrus:operation/name",
              producerOnly = true, remote = false, category = { 
Category.TESTING })
 public class CitrusEndpoint extends DefaultEndpoint {
 
@@ -51,7 +51,8 @@ public class CitrusEndpoint extends DefaultEndpoint {
     private volatile CountDownLatch latch;
 
     @UriPath(description = "The operation to perform. 'validate' validates 
exchanges without modifying them."
-                           + " 'receive' validates exchanges and returns a 
simulated response.")
+                           + " 'receive' validates exchanges and returns a 
simulated response.",
+             enums = "validate,receive")
     @Metadata(required = true)
     private String operation;
 
@@ -63,15 +64,15 @@ public class CitrusEndpoint extends DefaultEndpoint {
                             + " Supported values: json, xml, yaml, plaintext.")
     private String messageType;
 
-    private MessageValidationSpec allMessagesSpec;
+    private volatile MessageValidationSpec allMessagesSpec;
     private final Map<Integer, MessageValidationSpec> indexedSpecs = new 
LinkedHashMap<>();
-    private int nextSpecIndex;
+    private volatile int nextSpecIndex;
 
-    private MessageResponseSpec allMessagesResponseSpec;
+    private volatile MessageResponseSpec allMessagesResponseSpec;
     private final Map<Integer, MessageResponseSpec> indexedResponseSpecs = new 
LinkedHashMap<>();
-    private int nextResponseSpecIndex;
+    private volatile int nextResponseSpecIndex;
 
-    private int expectedCount = -1;
+    private volatile int expectedCount = -1;
 
     public CitrusEndpoint(String endpointUri, CitrusComponent component) {
         super(endpointUri, component);
@@ -116,6 +117,8 @@ public class CitrusEndpoint extends DefaultEndpoint {
 
     public CitrusEndpoint expectedMessageCount(int count) {
         this.expectedCount = count;
+        this.receivedCount.set(0);
+        this.receivedExchanges.clear();
         this.latch = new CountDownLatch(count);
         return this;
     }
@@ -160,16 +163,21 @@ public class CitrusEndpoint extends DefaultEndpoint {
     }
 
     public void assertIsSatisfied(long timeout, TimeUnit unit) throws 
InterruptedException {
+        int effectiveExpected = expectedCount;
+        if (effectiveExpected < 0 && !indexedSpecs.isEmpty()) {
+            effectiveExpected = indexedSpecs.size();
+        }
+
         if (latch != null && !latch.await(timeout, unit)) {
             throw new ValidationException(
                     "Timed out waiting for %d messages on endpoint: %s"
-                            .formatted(expectedCount, getEndpointUri()));
+                            .formatted(effectiveExpected, getEndpointUri()));
         }
 
-        if (expectedCount >= 0 && receivedCount.get() != expectedCount) {
+        if (effectiveExpected >= 0 && receivedCount.get() != 
effectiveExpected) {
             throw new ValidationException(
                     "Expected %d messages but received %d on endpoint: %s"
-                            .formatted(expectedCount, receivedCount.get(), 
getEndpointUri()));
+                            .formatted(effectiveExpected, receivedCount.get(), 
getEndpointUri()));
         }
 
         if (!failures.isEmpty()) {
diff --git 
a/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusProducer.java
 
b/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusProducer.java
index 407767faebfa..c565db25cba7 100644
--- 
a/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusProducer.java
+++ 
b/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusProducer.java
@@ -19,6 +19,7 @@ package org.apache.camel.component.citrus;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 import org.apache.camel.Exchange;
@@ -60,16 +61,25 @@ public class CitrusProducer extends DefaultProducer {
         citrusContext = CitrusContext.create();
     }
 
+    @Override
+    protected void doStop() throws Exception {
+        super.doStop();
+        if (citrusContext != null) {
+            citrusContext.close();
+            citrusContext = null;
+        }
+    }
+
     @Override
     public void process(Exchange exchange) throws Exception {
         CitrusEndpoint endpoint = getEndpoint();
 
+        int index = endpoint.nextExchangeIndex();
+        TestContext testContext = citrusContext.createTestContext();
+        propagateExchangeVariables(exchange, testContext);
+
         try {
-            int index = endpoint.nextExchangeIndex();
             MessageValidationSpec spec = endpoint.resolveValidationSpec(index);
-            TestContext testContext = citrusContext.createTestContext();
-            propagateExchangeProperties(exchange, testContext);
-
             if (spec != null) {
                 DefaultMessage receivedMessage = 
buildReceivedMessage(exchange);
                 DefaultMessage controlMessage = buildControlMessage(spec, 
testContext);
@@ -90,19 +100,35 @@ public class CitrusProducer extends DefaultProducer {
             }
         } catch (Exception e) {
             endpoint.addFailure(e);
-            throw e;
+            if ("receive".equals(endpoint.getOperation())) {
+                throw e;
+            }
         } finally {
+            writeBackExtractedVariables(exchange, testContext);
             endpoint.onExchangeReceived(exchange);
         }
     }
 
-    private void propagateExchangeProperties(Exchange exchange, TestContext 
testContext) {
-        Map<String, Object> properties = exchange.getProperties();
-        for (Map.Entry<String, Object> entry : properties.entrySet()) {
+    private void propagateExchangeVariables(Exchange exchange, TestContext 
testContext) {
+        for (Map.Entry<String, Object> entry : 
exchange.getProperties().entrySet()) {
             if (entry.getValue() != null) {
                 testContext.setVariable(entry.getKey(), entry.getValue());
             }
         }
+        for (Map.Entry<String, Object> entry : 
exchange.getVariables().entrySet()) {
+            if (entry.getValue() != null) {
+                testContext.setVariable(entry.getKey(), entry.getValue());
+            }
+        }
+    }
+
+    private void writeBackExtractedVariables(Exchange exchange, TestContext 
testContext) {
+        for (Map.Entry<String, Object> entry : 
testContext.getVariables().entrySet()) {
+            String key = entry.getKey();
+            if (!exchange.getProperties().containsKey(key) && 
!exchange.getVariables().containsKey(key)) {
+                exchange.setVariable(key, entry.getValue());
+            }
+        }
     }
 
     private DefaultMessage buildReceivedMessage(Exchange exchange) {
@@ -137,9 +163,9 @@ public class CitrusProducer extends DefaultProducer {
         String type = endpoint.getMessageType();
         if (type != null) {
             if (MessageType.knows(type)) {
-                return MessageType.valueOf(type.toUpperCase()).name();
+                return 
MessageType.valueOf(type.toUpperCase(Locale.ROOT)).name();
             } else {
-                return type.toUpperCase();
+                return type.toUpperCase(Locale.ROOT);
             }
         }
 
diff --git 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusMultipleEndpointsTest.java
 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusMultipleEndpointsTest.java
index 46e4d762d932..dbac357254af 100644
--- 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusMultipleEndpointsTest.java
+++ 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusMultipleEndpointsTest.java
@@ -18,7 +18,7 @@ package org.apache.camel.component.citrus;
 
 import org.apache.camel.RoutesBuilder;
 import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
diff --git 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusReceiveTest.java
 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusReceiveTest.java
index 101950cedcef..3c0e9a3b2f68 100644
--- 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusReceiveTest.java
+++ 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusReceiveTest.java
@@ -19,7 +19,7 @@ package org.apache.camel.component.citrus;
 import org.apache.camel.Exchange;
 import org.apache.camel.RoutesBuilder;
 import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
diff --git 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusSequentialValidationTest.java
 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusSequentialValidationTest.java
index 07e27a9d7d35..1ce218d5c3c1 100644
--- 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusSequentialValidationTest.java
+++ 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusSequentialValidationTest.java
@@ -16,10 +16,10 @@
  */
 package org.apache.camel.component.citrus;
 
-import org.apache.camel.CamelExecutionException;
 import org.apache.camel.RoutesBuilder;
 import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.citrusframework.exceptions.ValidationException;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -73,14 +73,16 @@ class CitrusSequentialValidationTest extends 
CamelTestSupport {
         CitrusEndpoint citrus = 
context.getEndpoint("citrus:validate/test?messageType=json", 
CitrusEndpoint.class);
 
         citrus.message(0).body("{\"event\": \"first\"}");
+        citrus.expectedMessageCount(2);
 
         template.sendBody("direct:start", "{\"event\": \"first\"}");
+        template.sendBody("direct:start", "{\"event\": \"second\"}");
 
-        assertThatThrownBy(() -> template.sendBody("direct:start", 
"{\"event\": \"second\"}"))
-                .isInstanceOf(CamelExecutionException.class)
-                .cause()
-                .isInstanceOf(IllegalStateException.class)
-                .hasMessageContaining("No validation specification for 
exchange at index 1");
+        assertThatThrownBy(citrus::assertIsSatisfied)
+                .isInstanceOf(ValidationException.class)
+                .satisfies(e -> assertThat(e.getSuppressed()[0])
+                        .isInstanceOf(IllegalStateException.class)
+                        .hasMessageContaining("No validation specification for 
exchange at index 1"));
     }
 
     @Test
@@ -121,11 +123,13 @@ class CitrusSequentialValidationTest extends 
CamelTestSupport {
 
         citrus.message(0).body("{\"event\": \"created\"}");
         citrus.message(1).body("{\"event\": \"updated\"}");
+        citrus.expectedMessageCount(2);
 
         template.sendBody("direct:start", "{\"event\": \"created\"}");
+        template.sendBody("direct:start", "{\"event\": \"wrong\"}");
 
-        assertThatThrownBy(() -> template.sendBody("direct:start", 
"{\"event\": \"wrong\"}"))
-                .isInstanceOf(CamelExecutionException.class);
+        assertThatThrownBy(citrus::assertIsSatisfied)
+                .isInstanceOf(ValidationException.class);
     }
 
     @Override
diff --git 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidateJsonTest.java
 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidateJsonTest.java
index def2607d68aa..30241c3e5e81 100644
--- 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidateJsonTest.java
+++ 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidateJsonTest.java
@@ -18,7 +18,8 @@ package org.apache.camel.component.citrus;
 
 import org.apache.camel.RoutesBuilder;
 import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.citrusframework.exceptions.ValidationException;
 import org.citrusframework.validation.DefaultValidations;
 import org.junit.jupiter.api.Test;
 
@@ -55,13 +56,16 @@ class CitrusValidateJsonTest extends CamelTestSupport {
                   "greeting": "Hello World!"
                 }
                 """);
+        citrus.expectedMessageCount(1);
 
-        assertThatThrownBy(() -> template.sendBody("direct:start", """
+        template.sendBody("direct:start", """
                 {
                   "greeting": "Goodbye!"
                 }
-                """))
-                .isInstanceOf(org.apache.camel.CamelExecutionException.class);
+                """);
+
+        assertThatThrownBy(citrus::assertIsSatisfied)
+                .isInstanceOf(ValidationException.class);
     }
 
     @Test
diff --git 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidatePlainTextTest.java
 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidatePlainTextTest.java
index a603f4ce6e15..f030a5061a85 100644
--- 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidatePlainTextTest.java
+++ 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidatePlainTextTest.java
@@ -16,10 +16,10 @@
  */
 package org.apache.camel.component.citrus;
 
-import org.apache.camel.CamelExecutionException;
 import org.apache.camel.RoutesBuilder;
 import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.citrusframework.exceptions.ValidationException;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -43,9 +43,12 @@ class CitrusValidatePlainTextTest extends CamelTestSupport {
     void shouldFailOnPlainTextMismatch() {
         CitrusEndpoint citrus = 
context.getEndpoint("citrus:validate?messageType=plaintext", 
CitrusEndpoint.class);
         citrus.allMessages().body("Hello Camel!");
+        citrus.expectedMessageCount(1);
+
+        template.sendBody("direct:start", "Goodbye Camel!");
 
-        assertThatThrownBy(() -> template.sendBody("direct:start", "Goodbye 
Camel!"))
-                .isInstanceOf(CamelExecutionException.class);
+        assertThatThrownBy(citrus::assertIsSatisfied)
+                .isInstanceOf(ValidationException.class);
     }
 
     @Test
diff --git 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidateXmlTest.java
 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidateXmlTest.java
index 9fff75cc0358..4b734fc3b65a 100644
--- 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidateXmlTest.java
+++ 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidateXmlTest.java
@@ -18,7 +18,7 @@ package org.apache.camel.component.citrus;
 
 import org.apache.camel.RoutesBuilder;
 import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.camel.test.junit6.CamelTestSupport;
 import org.citrusframework.validation.DefaultValidations;
 import org.junit.jupiter.api.Test;
 
diff --git 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidateYamlTest.java
 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidateYamlTest.java
index da4906f337ff..58680624f7ce 100644
--- 
a/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidateYamlTest.java
+++ 
b/components/camel-test/camel-test-citrus/src/test/java/org/apache/camel/component/citrus/CitrusValidateYamlTest.java
@@ -16,10 +16,10 @@
  */
 package org.apache.camel.component.citrus;
 
-import org.apache.camel.CamelExecutionException;
 import org.apache.camel.RoutesBuilder;
 import org.apache.camel.builder.RouteBuilder;
-import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.citrusframework.exceptions.ValidationException;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -88,11 +88,14 @@ class CitrusValidateYamlTest extends CamelTestSupport {
         citrus.allMessages().body("""
                 greeting: Hello World!
                 """);
+        citrus.expectedMessageCount(1);
 
-        assertThatThrownBy(() -> template.sendBody("direct:start", """
+        template.sendBody("direct:start", """
                 greeting: Goodbye!
-                """))
-                .isInstanceOf(CamelExecutionException.class);
+                """);
+
+        assertThatThrownBy(citrus::assertIsSatisfied)
+                .isInstanceOf(ValidationException.class);
     }
 
     @Override
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
index e56480791066..bef527f62ada 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
@@ -3379,7 +3379,7 @@ public class StaticEndpointBuilders {
      * Citrus message validation component for testing
      * 
      * Category: testing
-     * Since: 4.23
+     * Since: 4.24
      * Maven coordinates: org.apache.camel:camel-test-citrus
      * 
      * Syntax: <code>citrus:operation/name</code>
@@ -3388,6 +3388,7 @@ public class StaticEndpointBuilders {
      * The operation to perform. 'validate' validates exchanges without
      * modifying them. 'receive' validates exchanges and returns a simulated
      * response.
+     * There are 2 enums and the value can be one of: validate, receive
      * 
      * Path parameter: name
      * A unique name to identify this endpoint. Allows multiple Citrus 
endpoints
@@ -3404,7 +3405,7 @@ public class StaticEndpointBuilders {
      * Citrus message validation component for testing
      * 
      * Category: testing
-     * Since: 4.23
+     * Since: 4.24
      * Maven coordinates: org.apache.camel:camel-test-citrus
      * 
      * Syntax: <code>citrus:operation/name</code>
@@ -3413,6 +3414,7 @@ public class StaticEndpointBuilders {
      * The operation to perform. 'validate' validates exchanges without
      * modifying them. 'receive' validates exchanges and returns a simulated
      * response.
+     * There are 2 enums and the value can be one of: validate, receive
      * 
      * Path parameter: name
      * A unique name to identify this endpoint. Allows multiple Citrus 
endpoints
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/CitrusEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/CitrusEndpointBuilderFactory.java
index 1cff0c52ed04..8deac9e3c7a7 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/CitrusEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/CitrusEndpointBuilderFactory.java
@@ -125,7 +125,7 @@ public interface CitrusEndpointBuilderFactory {
          * Citrus message validation component for testing
          * 
          * Category: testing
-         * Since: 4.23
+         * Since: 4.24
          * Maven coordinates: org.apache.camel:camel-test-citrus
          * 
          * Syntax: <code>citrus:operation/name</code>
@@ -134,6 +134,7 @@ public interface CitrusEndpointBuilderFactory {
          * The operation to perform. 'validate' validates exchanges without
          * modifying them. 'receive' validates exchanges and returns a 
simulated
          * response.
+         * There are 2 enums and the value can be one of: validate, receive
          * 
          * Path parameter: name
          * A unique name to identify this endpoint. Allows multiple Citrus
@@ -150,7 +151,7 @@ public interface CitrusEndpointBuilderFactory {
          * Citrus message validation component for testing
          * 
          * Category: testing
-         * Since: 4.23
+         * Since: 4.24
          * Maven coordinates: org.apache.camel:camel-test-citrus
          * 
          * Syntax: <code>citrus:operation/name</code>
@@ -159,6 +160,7 @@ public interface CitrusEndpointBuilderFactory {
          * The operation to perform. 'validate' validates exchanges without
          * modifying them. 'receive' validates exchanges and returns a 
simulated
          * response.
+         * There are 2 enums and the value can be one of: validate, receive
          * 
          * Path parameter: name
          * A unique name to identify this endpoint. Allows multiple Citrus

Reply via email to