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 737a9994c060b3ac9d3b803b50cf12f7662e82b5 Author: Christoph Deppisch <[email protected]> AuthorDate: Thu Oct 15 08:56:40 2026 +0200 CAMEL-25378: Address review feedback on camel-test-citrus - Handle StreamCache body in buildReceivedMessage to prevent stream drain on pass-through validate operation - Filter Citrus-internal variables (citrus.*, CITRUS_*) in writeBackExtractedVariables to avoid polluting exchange variables - Fix documentation to use 1-based indexing (matching MockEndpoint) - Rewrite receive operation docs to match actual chained API Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../camel/catalog/docs/citrus-component.adoc | 55 ++++++++++++---------- .../src/main/docs/citrus-component.adoc | 55 ++++++++++++---------- .../citrus/CitrusValidationProcessor.java | 8 ++++ 3 files changed, 66 insertions(+), 52 deletions(-) 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 4400a11cfa3b..6866e5868f1e 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 @@ -261,7 +261,7 @@ template.sendBodyAndHeader("direct:start", "{\"event\": \"deleted\"}", "action", citrus.assertIsSatisfied(); ---- -You can also use `validate(index)` to target a specific exchange by its 0-based position. +You can also use `validate(index)` to target a specific exchange by its 1-based position. When both `validate(index)` and `validateAll()` are configured, the indexed spec takes priority — it replaces `validateAll()` for that exchange entirely (the two are never combined). The @@ -274,11 +274,11 @@ failure is thrown immediately as an exception. [source,java] ---- -citrus.validate(0).body("{\"event\": \"start\"}"); +citrus.validate(1).body("{\"event\": \"start\"}"); citrus.validateAll().body("{\"event\": \"progress\"}").header("type", "status"); -// Exchange 0 → validated against validate(0) only (validateAll() is NOT applied) -// Exchange 1, 2, ... → validated against validateAll() (body + header) +// Exchange 1 → validated against validate(1) only (validateAll() is NOT applied) +// Exchange 2, 3, ... → validated against validateAll() (body + header) ---- === Receive operation (request/response simulation) @@ -294,9 +294,9 @@ should respond with (`then`). This is useful for simulating backend services in CitrusEndpoint citrus = context.getEndpoint("citrus:receive/service?messageType=json", CitrusEndpoint.class); // Define expected request and simulated response -citrus.when().allMessages().receive() - .body("{\"action\": \"greet\"}"); -citrus.then().allMessages().respond() +citrus.when().receiveAll() + .body("{\"action\": \"greet\"}") + .then().respond() .body("{\"greeting\": \"Hello!\"}"); citrus.expectedMessageCount(1); @@ -311,15 +311,14 @@ assertThat(result.getIn().getBody(String.class)).isEqualTo("{\"greeting\": \"Hel ==== Gherkin-style API -The `when()` and `then()` methods provide a natural-language-like syntax: +The `when()` method returns a `WhenSpec` that provides a chained fluent syntax: -* `when().message(index).receive()` — define validation expectations for a specific exchange -* `when().allMessages().receive()` — define default validation for all exchanges -* `then().message(index).respond()` — define the response for a specific exchange -* `then().allMessages().respond()` — define the default response for all exchanges -* `then().message(index).send()` — alias for `respond()` +* `when().receive(index).body(...).then().respond()` — define validation and response for a specific exchange (1-based index) +* `when().receive().body(...).then().respond()` — auto-incrementing index for sequential exchanges +* `when().receiveAll().body(...).then().respond()` — define default validation and response for all exchanges +* `then().send()` — alias for `respond()` -Both the `receive()` spec and the `respond()` spec support `body()` and `header()` methods. +Both the `receive()` / `receiveAll()` spec and the `respond()` / `send()` spec support `body()` and `header()` methods. ==== Per-exchange request/response @@ -329,24 +328,26 @@ You can define different request expectations and responses for each exchange in ---- CitrusEndpoint citrus = context.getEndpoint("citrus:receive/service?messageType=json", CitrusEndpoint.class); -citrus.when().message(0).receive().body("{\"action\": \"create\"}"); -citrus.then().message(0).respond().body("{\"id\": \"1\", \"status\": \"created\"}"); +citrus.when().receive(1).body("{\"action\": \"create\"}") + .then().respond().body("{\"id\": \"1\", \"status\": \"created\"}"); -citrus.when().message(1).receive().body("{\"action\": \"update\"}"); -citrus.then().message(1).respond().body("{\"id\": \"1\", \"status\": \"updated\"}"); +citrus.when().receive(2).body("{\"action\": \"update\"}") + .then().respond().body("{\"id\": \"1\", \"status\": \"updated\"}"); citrus.expectedMessageCount(2); ---- -Indexed specs take priority over `allMessages()` for both request validation and response -simulation. When a specific exchange has no indexed spec, the `allMessages()` spec is used as +Indexed specs take priority over `receiveAll()` for both request validation and response +simulation. When a specific exchange has no indexed spec, the `receiveAll()` spec is used as a fallback. ==== Response with headers [source,java] ---- -citrus.then().allMessages().respond() +citrus.when().receiveAll() + .body("{\"action\": \"greet\"}") + .then().respond() .body("{\"greeting\": \"Hello!\"}") .header("Content-Type", "application/json") .header("X-Status", "ok"); @@ -354,25 +355,27 @@ citrus.then().allMessages().respond() ==== Citrus variables in responses -Exchange properties are available as Citrus test variables in response templates. This enables -dynamic response generation based on the incoming request context: +Exchange properties and exchange variables are available as Citrus test variables in response +templates. This enables dynamic response generation based on the incoming request context: [source,java] ---- -citrus.then().allMessages().respond() +citrus.when().receiveAll() + .body("{\"action\": \"greet\", \"name\": \"@ignore@\"}") + .then().respond() .body("{\"greeting\": \"Hello ${userName}!\"}") .header("X-Correlation-Id", "${correlationId}"); ---- ==== Response-only mode -When you only need response simulation without request validation, omit the `when()` specification: +When you only need response simulation without request validation, omit the body on `receive()`: [source,java] ---- CitrusEndpoint citrus = context.getEndpoint("citrus:receive/responder", CitrusEndpoint.class); -citrus.then().allMessages().respond() +citrus.when().receive().then().respond() .body("OK") .header("status", "200"); citrus.expectedMessageCount(1); 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 4400a11cfa3b..6866e5868f1e 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 @@ -261,7 +261,7 @@ template.sendBodyAndHeader("direct:start", "{\"event\": \"deleted\"}", "action", citrus.assertIsSatisfied(); ---- -You can also use `validate(index)` to target a specific exchange by its 0-based position. +You can also use `validate(index)` to target a specific exchange by its 1-based position. When both `validate(index)` and `validateAll()` are configured, the indexed spec takes priority — it replaces `validateAll()` for that exchange entirely (the two are never combined). The @@ -274,11 +274,11 @@ failure is thrown immediately as an exception. [source,java] ---- -citrus.validate(0).body("{\"event\": \"start\"}"); +citrus.validate(1).body("{\"event\": \"start\"}"); citrus.validateAll().body("{\"event\": \"progress\"}").header("type", "status"); -// Exchange 0 → validated against validate(0) only (validateAll() is NOT applied) -// Exchange 1, 2, ... → validated against validateAll() (body + header) +// Exchange 1 → validated against validate(1) only (validateAll() is NOT applied) +// Exchange 2, 3, ... → validated against validateAll() (body + header) ---- === Receive operation (request/response simulation) @@ -294,9 +294,9 @@ should respond with (`then`). This is useful for simulating backend services in CitrusEndpoint citrus = context.getEndpoint("citrus:receive/service?messageType=json", CitrusEndpoint.class); // Define expected request and simulated response -citrus.when().allMessages().receive() - .body("{\"action\": \"greet\"}"); -citrus.then().allMessages().respond() +citrus.when().receiveAll() + .body("{\"action\": \"greet\"}") + .then().respond() .body("{\"greeting\": \"Hello!\"}"); citrus.expectedMessageCount(1); @@ -311,15 +311,14 @@ assertThat(result.getIn().getBody(String.class)).isEqualTo("{\"greeting\": \"Hel ==== Gherkin-style API -The `when()` and `then()` methods provide a natural-language-like syntax: +The `when()` method returns a `WhenSpec` that provides a chained fluent syntax: -* `when().message(index).receive()` — define validation expectations for a specific exchange -* `when().allMessages().receive()` — define default validation for all exchanges -* `then().message(index).respond()` — define the response for a specific exchange -* `then().allMessages().respond()` — define the default response for all exchanges -* `then().message(index).send()` — alias for `respond()` +* `when().receive(index).body(...).then().respond()` — define validation and response for a specific exchange (1-based index) +* `when().receive().body(...).then().respond()` — auto-incrementing index for sequential exchanges +* `when().receiveAll().body(...).then().respond()` — define default validation and response for all exchanges +* `then().send()` — alias for `respond()` -Both the `receive()` spec and the `respond()` spec support `body()` and `header()` methods. +Both the `receive()` / `receiveAll()` spec and the `respond()` / `send()` spec support `body()` and `header()` methods. ==== Per-exchange request/response @@ -329,24 +328,26 @@ You can define different request expectations and responses for each exchange in ---- CitrusEndpoint citrus = context.getEndpoint("citrus:receive/service?messageType=json", CitrusEndpoint.class); -citrus.when().message(0).receive().body("{\"action\": \"create\"}"); -citrus.then().message(0).respond().body("{\"id\": \"1\", \"status\": \"created\"}"); +citrus.when().receive(1).body("{\"action\": \"create\"}") + .then().respond().body("{\"id\": \"1\", \"status\": \"created\"}"); -citrus.when().message(1).receive().body("{\"action\": \"update\"}"); -citrus.then().message(1).respond().body("{\"id\": \"1\", \"status\": \"updated\"}"); +citrus.when().receive(2).body("{\"action\": \"update\"}") + .then().respond().body("{\"id\": \"1\", \"status\": \"updated\"}"); citrus.expectedMessageCount(2); ---- -Indexed specs take priority over `allMessages()` for both request validation and response -simulation. When a specific exchange has no indexed spec, the `allMessages()` spec is used as +Indexed specs take priority over `receiveAll()` for both request validation and response +simulation. When a specific exchange has no indexed spec, the `receiveAll()` spec is used as a fallback. ==== Response with headers [source,java] ---- -citrus.then().allMessages().respond() +citrus.when().receiveAll() + .body("{\"action\": \"greet\"}") + .then().respond() .body("{\"greeting\": \"Hello!\"}") .header("Content-Type", "application/json") .header("X-Status", "ok"); @@ -354,25 +355,27 @@ citrus.then().allMessages().respond() ==== Citrus variables in responses -Exchange properties are available as Citrus test variables in response templates. This enables -dynamic response generation based on the incoming request context: +Exchange properties and exchange variables are available as Citrus test variables in response +templates. This enables dynamic response generation based on the incoming request context: [source,java] ---- -citrus.then().allMessages().respond() +citrus.when().receiveAll() + .body("{\"action\": \"greet\", \"name\": \"@ignore@\"}") + .then().respond() .body("{\"greeting\": \"Hello ${userName}!\"}") .header("X-Correlation-Id", "${correlationId}"); ---- ==== Response-only mode -When you only need response simulation without request validation, omit the `when()` specification: +When you only need response simulation without request validation, omit the body on `receive()`: [source,java] ---- CitrusEndpoint citrus = context.getEndpoint("citrus:receive/responder", CitrusEndpoint.class); -citrus.then().allMessages().respond() +citrus.when().receive().then().respond() .body("OK") .header("status", "200"); citrus.expectedMessageCount(1); diff --git a/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusValidationProcessor.java b/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusValidationProcessor.java index 6b76f5d1966f..436aa0a6409e 100644 --- a/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusValidationProcessor.java +++ b/components/camel-test/camel-test-citrus/src/main/java/org/apache/camel/component/citrus/CitrusValidationProcessor.java @@ -24,6 +24,7 @@ import java.util.Map; import org.apache.camel.Exchange; import org.apache.camel.Processor; +import org.apache.camel.StreamCache; import org.citrusframework.CitrusContext; import org.citrusframework.context.TestContext; import org.citrusframework.message.DefaultMessage; @@ -90,6 +91,10 @@ class CitrusValidationProcessor implements Processor { private DefaultMessage buildReceivedMessage(Exchange exchange) { Object body = exchange.getIn().getBody(); + if (body instanceof StreamCache sc) { + body = exchange.getIn().getBody(String.class); + sc.reset(); + } Map<String, Object> headers = new LinkedHashMap<>(exchange.getIn().getHeaders()); return new DefaultMessage(body, headers); } @@ -207,6 +212,9 @@ class CitrusValidationProcessor implements Processor { private void writeBackExtractedVariables(Exchange exchange, TestContext testContext) { for (Map.Entry<String, Object> entry : testContext.getVariables().entrySet()) { String key = entry.getKey(); + if (key.startsWith("citrus.") || key.startsWith("CITRUS_")) { + continue; + } if (!exchange.getProperties().containsKey(key) && !exchange.getVariables().containsKey(key)) { exchange.setVariable(key, entry.getValue()); }
