christophd commented on code in PR #27792:
URL: https://github.com/apache/camel/pull/27792#discussion_r4286470936


##########
components/camel-test/camel-test-citrus/pom.xml:
##########
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.camel</groupId>
+        <artifactId>camel-test-parent</artifactId>
+        <version>4.23.0-SNAPSHOT</version>

Review Comment:
   Fixed: rebased on `main` (now `4.24.0-SNAPSHOT`) and updated the parent 
version in pom.xml and BOM.
   
   _Claude Code on behalf of @christophd_



##########
components/camel-test/camel-test-citrus/src/main/docs/citrus-component.adoc:
##########
@@ -0,0 +1,435 @@
+= Citrus Component
+:doctitle: Citrus
+:shortname: citrus
+:artifactid: camel-test-citrus
+:description: Citrus message validation component for testing
+:since: 4.24
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The Citrus component integrates with the https://citrusframework.org/[Citrus 
integration test framework]
+to perform message validation on exchanges arriving at a Camel endpoint. It 
allows test authors to
+verify exchange body content, headers, and path expressions (JsonPath, XPath) 
using Citrus validation
+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
+
+----
+citrus:operation/name[?options]
+----
+
+Where `operation` is `validate` or `receive`, and `name` is an optional 
identifier to distinguish
+multiple Citrus endpoints in the same route.
+
+* `validate` — validates exchanges without modifying them (pass-through 
validation). Validation
+  failures are collected silently and only surface when `assertIsSatisfied()` 
is called.
+* `receive` — validates exchanges and returns a simulated response 
(request/response simulation).
+  Validation failures remain on the exchange and trigger Camel's `onException` 
handlers.
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+== Usage
+
+The Citrus endpoint acts as a producer-only endpoint. Routes send exchanges 
*to* `citrus:validate`,
+and the component validates each exchange using Citrus message validation.
+
+NOTE: Spec setup (`validate()`, `validateAll()`, `when().receive()`) must 
complete before exchanges
+start arriving. For async consumers (SEDA, timer-based routes), ensure all 
validation chains are
+fully configured before starting the `CamelContext`.
+
+=== Validation modules
+
+The component does not bundle Citrus validation modules. You must add the 
required validation
+module dependency for your message type to your test classpath:
+
+[cols="1,2", options="header"]
+|===
+| Dependency | Description
+
+| `citrus-validation-json`
+| JSON message validation with JsonPath expressions and JSON schema support
+
+| `citrus-validation-xml`
+| XML message validation with XPath expressions, XML schema (XSD/WSDL) and 
XSLT support
+
+| `citrus-validation-text`
+| Plain text message validation including binary (stream and base64) and gzip 
support
+
+| `citrus-validation-yaml`
+| YAML message validation
+
+| `citrus-validation-binary`
+| Binary message validation with stream comparison and base64 encoding
+
+| `citrus-validation-hamcrest`
+| Hamcrest matcher assertions for use with JsonPath and XPath expressions
+|===
+
+All modules use the `org.citrusframework` group ID. Refer to the
+https://citrusframework.org/citrus/reference/html/[Citrus reference 
documentation] for detailed
+usage of each validation module.
+
+=== Citrus empowered message validation
+
+Citrus uses a powerful template-based approach to message validation: you 
provide an expected message template and
+Citrus compares it against the received message using format-aware comparison. 
This brings several
+capabilities beyond simple string equality.
+
+==== Ignore expressions
+
+Use the `@ignore@` placeholder anywhere in an expected message template to 
skip validation of dynamic
+values such as timestamps, generated IDs, or correlation tokens. The element 
must be present, but its
+value is not checked.
+
+[source,json]
+----
+{
+  "orderId": "@ignore@",
+  "timestamp": "@ignore@",
+  "status": "created"
+}
+----
+
+This works across all supported message types — in XML element and attribute 
values, JSON property values,
+and YAML property values. The ignore placeholder can also replace an entire 
sub-object or element
+including its children.
+
+For plain text messages, the `@ignore@` placeholder works on a positional 
basis — it matches a word-length
+character sequence at the exact position in the text. Use `@ignore(n)@` to 
ignore exactly _n_ characters
+when the value contains non-word characters:
+
+[source,plaintext]
+----
+Your current id is "@ignore@", it expires on 01/01/2030
+----
+
+This ignores the word following `id is "` based on the position of the ignore 
expression in the template.
+Because no character count is specified, Citrus ignores the entire word 
regardless of its length.
+The word boundary is determined by the first non-word character (whitespace, 
punctuation, etc.).
+
+[source,plaintext]
+----
+Your current id is "@ignore(12)@", it expires on @ignore(10)@
+----
+
+This ignores the next 12 characters starting at the position of the ignore 
expression in the template.
+This also includes whitespace and non-word characters (e.g. `Your current id 
is "1234/ 34567 D", it expires on 01/01/2030`).
+
+==== Validation matchers
+
+Citrus validation matchers are inline assertions embedded directly in the 
expected message template,
+surrounded by `@` markers. They work across all message formats (JSON, XML, 
YAML, plain text) and
+provide richer assertions than exact equality:
+
+[source,json]
+----
+{
+  "name": "@equalsIgnoreCase('JOHN')@",
+  "age": "@greaterThan(17)@",
+  "email": "@matches('[a-z]+@[a-z]+\\.[a-z]+')@",
+  "created": "@matchesDatePattern('yyyy-MM-dd')@",
+  "id": "@isNumber()@",
+  "token": "@startsWith('tk-')@",
+  "status": "@contains('active')@"
+}
+----
+
+You can also use `@variable('name')@` to extract the actual value into a 
Citrus test variable for
+later use, and `@assertThat(hamcrestMatcher)@` to use Hamcrest matchers inline 
(requires
+`citrus-validation-hamcrest`):
+
+[source,json]
+----
+{
+  "count": "@assertThat(allOf(greaterThan(0), lessThan(100)))@",
+  "type": "@assertThat(anyOf(equalTo(A), equalTo(B)))@"
+}
+----
+
+==== JSON validation
+
+Citrus JSON validation compares JSON object structures regardless of property 
order.
+It supports two modes configurable via `citrus.json.message.validation.strict`:
+
+* **Strict mode** (default): the received JSON must match the expected 
structure exactly — no
+  additional properties or array items are allowed.
+* **Non-strict mode**: the expected JSON can be a subset of the received 
message — additional
+  properties and array items in the received JSON are silently ignored. This 
is useful when you
+  only want to verify specific fields.
+
+JSON validation also supports:
+
+* **JsonPath expressions** for targeted field validation without specifying 
the full message body
+* **JSON schema validation** against JSON Schema V3/V4 definitions
+* **`@ignore@` and validation matchers** within JSON values
+
+==== XML validation
+
+Citrus XML validation performs a structural DOM tree comparison that correctly 
handles:
+
+* **XML namespaces** with different prefixes — the comparison is 
namespace-aware, so
+  `<ns1:element xmlns:ns1="...">` and `<ns2:element xmlns:ns2="...">` are 
treated as equivalent
+  when they share the same namespace URI.
+* **XML schema validation** against XSD and WSDL definitions to verify 
structural correctness.
+* **XPath expressions** for targeted element validation, with full namespace 
support via
+  namespace context configuration. This includes automatic namespace prefix 
resolution when the
+  prefixes differ between the control template and the received message.
+* **`@ignore@` and validation matchers** within XML element values and 
attributes.
+
+==== Plain text validation
+
+Plain text validation performs exact string comparison with support for:
+
+* **Positional `@ignore@`** to skip dynamic word-length sequences at specific 
positions.
+* **`@ignore(n)@`** to skip exactly _n_ characters (useful for values with 
non-word characters).
+* **`@variable('name')@`** to extract values at specific positions into test 
variables.
+* **Whitespace normalization** (configurable) to ignore line separator 
differences (CR, LF, CRLF)
+  or all whitespace.
+* **Gzip** and **binary/base64** content comparison via the respective message 
types.
+
+==== YAML validation
+
+YAML validation compares YAML documents structurally, similar to JSON 
validation:
+
+* **Strict/non-strict mode** — configurable via 
`citrus.yaml.message.validation.strict`.
+* **Path expressions** for ignoring specific elements in the YAML structure.
+* **`@ignore@` and validation matchers** within YAML values.
+
+=== Validation specification API
+
+The endpoint provides a fluent API for specifying validation expectations. You 
can define a single
+validation that applies to all incoming exchanges, or specify individual 
validations for each
+exchange in sequence.
+
+==== Single validation for all exchanges
+
+Use `validateAll()` to define one validation specification that is applied to 
every exchange.
+The returned `MessageValidationSpec` provides a fluent API with `body()`, 
`header()`, and
+`validate()` methods:
+
+[source,java]
+----
+CitrusEndpoint citrus = 
context.getEndpoint("citrus:validate?messageType=json", CitrusEndpoint.class);
+citrus.validateAll()
+    .body("{\"status\": \"ok\"}")
+    .header("operation", "test");
+citrus.expectedMessageCount(3);
+
+// All three exchanges are validated against the same spec
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}", 
"operation", "test");
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}", 
"operation", "test");
+template.sendBodyAndHeader("direct:start", "{\"status\": \"ok\"}", 
"operation", "test");
+
+citrus.assertIsSatisfied();
+----
+
+==== Per-exchange sequential validation
+
+Use `validate()` to specify different validation expectations for the 1st, 
2nd, 3rd, etc.
+exchange. Each call to `validate()` auto-increments the index, so specs are 
applied in order.
+Each incoming exchange triggers the next validation spec in sequence:
+
+[source,java]
+----
+CitrusEndpoint citrus = 
context.getEndpoint("citrus:validate?messageType=json", CitrusEndpoint.class);
+
+citrus.validate().body("{\"event\": \"created\"}").header("action", "create");
+citrus.validate().body("{\"event\": \"updated\"}").header("action", "update");
+citrus.validate().body("{\"event\": \"deleted\"}").header("action", "delete");
+citrus.expectedMessageCount(3);
+
+template.sendBodyAndHeader("direct:start", "{\"event\": \"created\"}", 
"action", "create");
+template.sendBodyAndHeader("direct:start", "{\"event\": \"updated\"}", 
"action", "update");
+template.sendBodyAndHeader("direct:start", "{\"event\": \"deleted\"}", 
"action", "delete");
+
+citrus.assertIsSatisfied();
+----
+
+You can also use `validate(index)` to target a specific exchange by its 
1-based position.
+
+Indexed specs (`validate()`, `validate(index)`, `when().receive()`) imply a 
minimum message count equal to the highest index, unless the test sets 
`expectedMessageCount(...)` or a larger `expectedMinimumMessageCount(...)`. So 
`assertIsSatisfied()` fails when fewer exchanges arrive.
+
+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
+`validateAll()` 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 
`validateAll()` fallback,
+the exchange passes through without validation. No failure is recorded by the 
endpoint.
+For the `validate` operation, unmatched exchanges are ignored silently. For 
the `receive` operation,
+no simulated response is applied and the exchange continues unchanged.
+
+[source,java]
+----
+citrus.validate(1).body("{\"event\": \"start\"}");
+citrus.validateAll().body("{\"event\": \"progress\"}").header("type", 
"status");
+
+// Exchange 1 → validated against validate(1) only (validateAll() is NOT 
applied)
+// Exchange 2, 3, ... → validated against validateAll() (body + header)
+----
+
+=== Receive operation (request/response simulation)
+
+The `receive` operation extends the validation endpoint with response 
simulation. It uses a
+Gherkin-style fluent API to define what the endpoint expects to receive 
(`when`) and what it
+should respond with (`then`). This is useful for simulating backend services 
in route tests.
+
+==== Basic usage
+
+[source,java]
+----
+CitrusEndpoint citrus = 
context.getEndpoint("citrus:receive/service?messageType=json", 
CitrusEndpoint.class);
+
+// Define expected request and simulated response
+citrus.when().receiveAll()
+    .body("{\"action\": \"greet\"}")
+    .then().respond()
+    .body("{\"greeting\": \"Hello!\"}");
+citrus.expectedMessageCount(1);
+
+// Send a request and verify the response
+Exchange result = template.send("direct:start", exchange -> {
+    exchange.getIn().setBody("{\"action\": \"greet\"}");
+});
+
+citrus.assertIsSatisfied();
+assertThat(result.getIn().getBody(String.class)).isEqualTo("{\"greeting\": 
\"Hello!\"}");
+----
+
+==== Gherkin-style API
+
+The `when()` method returns a `WhenSpec` that provides a chained fluent syntax:
+
+* `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()` / `receiveAll()` spec and the `respond()` / `send()` spec 
support `body()` and `header()` methods.
+
+==== Per-exchange request/response
+
+You can define different request expectations and responses for each exchange 
in sequence:
+
+[source,java]
+----
+CitrusEndpoint citrus = 
context.getEndpoint("citrus:receive/service?messageType=json", 
CitrusEndpoint.class);
+
+citrus.when().receive(1).body("{\"action\": \"create\"}")
+    .then().respond().body("{\"id\": \"1\", \"status\": \"created\"}");
+
+citrus.when().receive(2).body("{\"action\": \"update\"}")
+    .then().respond().body("{\"id\": \"1\", \"status\": \"updated\"}");
+
+citrus.expectedMessageCount(2);
+----
+
+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.when().receiveAll()
+    .body("{\"action\": \"greet\"}")
+    .then().respond()
+    .body("{\"greeting\": \"Hello!\"}")
+    .header("Content-Type", "application/json")
+    .header("X-Status", "ok");
+----
+
+==== Citrus variables in responses
+
+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.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 
body on `receive()`:
+
+[source,java]
+----
+CitrusEndpoint citrus = context.getEndpoint("citrus:receive/responder", 
CitrusEndpoint.class);
+
+citrus.when().receive().then().respond()
+    .body("OK")
+    .header("status", "200");
+citrus.expectedMessageCount(1);
+----
+
+=== Error handling and redelivery
+
+The two operations handle validation failures differently with respect to 
Camel's error handling:
+
+* **`validate` operation** — validation failures are captured internally and 
the exception is
+  cleared from the exchange. Camel's `onException` handlers and redelivery 
policies do *not*
+  trigger. Failures surface only when `assertIsSatisfied()` is called.
+* **`receive` operation** — validation failures remain on the exchange as an 
exception. Camel's
+  `onException` handlers and redelivery policies *do* trigger, which matches 
the expected
+  behavior for a request/response endpoint where callers need to see errors.
+
+NOTE: Each redelivery attempt counts as a new exchange in the mock counter. So 
if a
+`receive(1)` spec fails and Camel redelivers, the retry is exchange 2 and
+`receive(2)` applies to it — not `receive(1)` again.

Review Comment:
   Applied the suggestion — the NOTE now mentions that if no spec exists for 
the retry, the earlier failure is not recorded, and advises setting 
`expectedMessageCount(...)` to catch the extra exchange.
   
   _Claude Code on behalf of @christophd_



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to