diff --git a/components/camel-rest/pom.xml b/components/camel-rest/pom.xml
index e78a91271f69e..f165758b49c7e 100644
--- a/components/camel-rest/pom.xml
+++ b/components/camel-rest/pom.xml
@@ -39,5 +39,28 @@
camel-support
+
+
+ org.apache.camel
+ camel-test-junit5
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ ${mockito-version}
+ test
+
+
+ org.assertj
+ assertj-core
+ test
+
+
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/DefaultRestRegistryTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/DefaultRestRegistryTest.java
new file mode 100644
index 0000000000000..136aab89d2f42
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/DefaultRestRegistryTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.rest;
+
+import java.util.List;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Consumer;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.RestRegistry;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+class DefaultRestRegistryTest {
+
+ private DefaultRestRegistry registry;
+ private CamelContext camelContext;
+
+ @Mock
+ private Consumer consumer1;
+
+ @Mock
+ private Consumer consumer2;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ registry = new DefaultRestRegistry();
+ registry.setCamelContext(camelContext);
+ registry.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ registry.stop();
+ camelContext.stop();
+ }
+
+ @Test
+ void testAddRestService() {
+ registry.addRestService(consumer1, false, "http://localhost:8080/api/users",
+ "http://localhost:8080", "/api", "/users", "GET",
+ "application/json", "application/json", "User", "User",
+ "route1", "Get all users");
+
+ assertThat(registry.size()).isEqualTo(1);
+ }
+
+ @Test
+ void testAddMultipleRestServices() {
+ registry.addRestService(consumer1, false, "http://localhost:8080/api/users",
+ "http://localhost:8080", "/api", "/users", "GET",
+ "application/json", "application/json", null, null,
+ "route1", "Get all users");
+
+ registry.addRestService(consumer2, false, "http://localhost:8080/api/orders",
+ "http://localhost:8080", "/api", "/orders", "POST",
+ "application/json", "application/json", "Order", "Order",
+ "route2", "Create order");
+
+ assertThat(registry.size()).isEqualTo(2);
+ }
+
+ @Test
+ void testAddMultipleServicesToSameConsumer() {
+ registry.addRestService(consumer1, false, "http://localhost:8080/api/users",
+ "http://localhost:8080", "/api", "/users", "GET",
+ null, null, null, null, "route1", "Get users");
+
+ registry.addRestService(consumer1, false, "http://localhost:8080/api/users/{id}",
+ "http://localhost:8080", "/api", "/users/{id}", "GET",
+ null, null, null, null, "route2", "Get user by id");
+
+ assertThat(registry.size()).isEqualTo(2);
+ }
+
+ @Test
+ void testRemoveRestService() {
+ registry.addRestService(consumer1, false, "http://localhost:8080/api/users",
+ "http://localhost:8080", "/api", "/users", "GET",
+ null, null, null, null, "route1", "Get users");
+
+ assertThat(registry.size()).isEqualTo(1);
+
+ registry.removeRestService(consumer1);
+
+ assertThat(registry.size()).isEqualTo(0);
+ }
+
+ @Test
+ void testRemoveNonExistentService() {
+ registry.addRestService(consumer1, false, "http://localhost:8080/api/users",
+ "http://localhost:8080", "/api", "/users", "GET",
+ null, null, null, null, "route1", "Get users");
+
+ registry.removeRestService(consumer2);
+
+ assertThat(registry.size()).isEqualTo(1);
+ }
+
+ @Test
+ void testListAllRestServices() {
+ registry.addRestService(consumer1, false, "http://localhost:8080/api/users",
+ "http://localhost:8080", "/api", "/users", "GET",
+ "application/json", "application/json", "User", "UserResponse",
+ "route1", "Get all users");
+
+ List services = registry.listAllRestServices();
+
+ assertThat(services).hasSize(1);
+
+ RestRegistry.RestService service = services.get(0);
+ assertThat(service.getUrl()).isEqualTo("http://localhost:8080/api/users");
+ assertThat(service.getBaseUrl()).isEqualTo("http://localhost:8080");
+ assertThat(service.getBasePath()).isEqualTo("/api");
+ assertThat(service.getUriTemplate()).isEqualTo("/users");
+ assertThat(service.getMethod()).isEqualTo("GET");
+ assertThat(service.getConsumes()).isEqualTo("application/json");
+ assertThat(service.getProduces()).isEqualTo("application/json");
+ assertThat(service.getInType()).isEqualTo("User");
+ assertThat(service.getOutType()).isEqualTo("UserResponse");
+ assertThat(service.getDescription()).isEqualTo("Get all users");
+ assertThat(service.getConsumer()).isSameAs(consumer1);
+ assertThat(service.isContractFirst()).isFalse();
+ }
+
+ @Test
+ void testContractFirstService() {
+ registry.addRestService(consumer1, true, "http://localhost:8080/api/users",
+ "http://localhost:8080", "/api", "/users", "GET",
+ null, null, null, null, "route1", "Get users");
+
+ List services = registry.listAllRestServices();
+ assertThat(services.get(0).isContractFirst()).isTrue();
+ }
+
+ @Test
+ void testSizeWithEmptyRegistry() {
+ assertThat(registry.size()).isEqualTo(0);
+ }
+
+ @Test
+ void testListAllRestServicesEmpty() {
+ List services = registry.listAllRestServices();
+ assertThat(services).isEmpty();
+ }
+
+ @Test
+ void testServiceState() {
+ registry.addRestService(consumer1, false, "http://localhost:8080/api/users",
+ "http://localhost:8080", "/api", "/users", "GET",
+ null, null, null, null, "route1", "Get users");
+
+ List services = registry.listAllRestServices();
+ // Non-stateful consumer returns Stopped state
+ assertThat(services.get(0).getState()).isEqualTo("Stopped");
+ }
+
+ @Test
+ void testApiDocAsJsonWithNoEndpoints() {
+ String apiDoc = registry.apiDocAsJson();
+ assertThat(apiDoc).isNull();
+ }
+
+ @Test
+ void testStopClearsRegistry() throws Exception {
+ registry.addRestService(consumer1, false, "http://localhost:8080/api/users",
+ "http://localhost:8080", "/api", "/users", "GET",
+ null, null, null, null, "route1", "Get users");
+
+ assertThat(registry.size()).isEqualTo(1);
+
+ registry.stop();
+
+ assertThat(registry.size()).isEqualTo(0);
+ }
+}
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestApiComponentTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestApiComponentTest.java
new file mode 100644
index 0000000000000..55cc958909fbf
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestApiComponentTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.rest;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class RestApiComponentTest {
+
+ private CamelContext camelContext;
+ private RestApiComponent component;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ component = new RestApiComponent();
+ component.setCamelContext(camelContext);
+ camelContext.addComponent("rest-api", component);
+ camelContext.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ camelContext.stop();
+ }
+
+ @Test
+ void testCreateEndpoint() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest-api:api-doc");
+
+ assertThat(endpoint).isNotNull();
+ assertThat(endpoint).isInstanceOf(RestApiEndpoint.class);
+
+ RestApiEndpoint restApiEndpoint = (RestApiEndpoint) endpoint;
+ assertThat(restApiEndpoint.getPath()).isEqualTo("api-doc");
+ }
+
+ @Test
+ void testCreateEndpointWithPath() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest-api:swagger/v2");
+
+ RestApiEndpoint restApiEndpoint = (RestApiEndpoint) endpoint;
+ assertThat(restApiEndpoint.getPath()).isEqualTo("swagger/v2");
+ }
+
+ @Test
+ void testCreateEndpointWithConsumerComponentName() throws Exception {
+ component.setConsumerComponentName("undertow");
+ Endpoint endpoint = camelContext.getEndpoint("rest-api:api-doc");
+
+ RestApiEndpoint restApiEndpoint = (RestApiEndpoint) endpoint;
+ assertThat(restApiEndpoint.getConsumerComponentName()).isEqualTo("undertow");
+ }
+
+ @Test
+ void testCreateEndpointWithApiComponentName() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest-api:api-doc?apiComponentName=swagger");
+
+ RestApiEndpoint restApiEndpoint = (RestApiEndpoint) endpoint;
+ assertThat(restApiEndpoint.getApiComponentName()).isEqualTo("swagger");
+ }
+
+ @Test
+ void testCreateEndpointWithConsumerComponentNameParam() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest-api:api-doc?consumerComponentName=netty-http");
+
+ RestApiEndpoint restApiEndpoint = (RestApiEndpoint) endpoint;
+ assertThat(restApiEndpoint.getConsumerComponentName()).isEqualTo("netty-http");
+ }
+
+ @Test
+ void testComponentConsumerNameOverriddenByParam() throws Exception {
+ component.setConsumerComponentName("jetty");
+ Endpoint endpoint = camelContext.getEndpoint("rest-api:api-doc?consumerComponentName=servlet");
+
+ RestApiEndpoint restApiEndpoint = (RestApiEndpoint) endpoint;
+ // Parameter should override component setting
+ assertThat(restApiEndpoint.getConsumerComponentName()).isEqualTo("servlet");
+ }
+}
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestApiEndpointFactoryTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestApiEndpointFactoryTest.java
new file mode 100644
index 0000000000000..eaa1b8357042e
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestApiEndpointFactoryTest.java
@@ -0,0 +1,268 @@
+/*
+ * 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.rest;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Consumer;
+import org.apache.camel.NoSuchBeanException;
+import org.apache.camel.Processor;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.RestApiConsumerFactory;
+import org.apache.camel.spi.RestApiProcessorFactory;
+import org.apache.camel.spi.RestConfiguration;
+import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.support.SimpleRegistry;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class RestApiEndpointFactoryTest {
+
+ private CamelContext camelContext;
+ private RestApiComponent component;
+ private SimpleRegistry registry;
+
+ @Mock
+ private Consumer mockConsumer;
+
+ @Mock
+ private Processor mockProcessor;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ registry = new SimpleRegistry();
+ camelContext = new DefaultCamelContext(registry);
+ component = new RestApiComponent();
+ component.setCamelContext(camelContext);
+ camelContext.addComponent("rest-api", component);
+ camelContext.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ camelContext.stop();
+ }
+
+ @Test
+ void testCreateConsumerWithExplicitConsumerComponentName() throws Exception {
+ // Register a mock RestApiConsumerFactory in registry
+ RestApiConsumerFactory mockFactory = mock(RestApiConsumerFactory.class);
+ when(mockFactory.createApiConsumer(
+ any(CamelContext.class),
+ any(Processor.class),
+ eq("/api-doc"),
+ any(RestConfiguration.class),
+ any(Map.class))).thenReturn(mockConsumer);
+
+ registry.bind("myApiFactory", mockFactory);
+
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:api-doc");
+ endpoint.setConsumerComponentName("myApiFactory");
+ endpoint.setParameters(new HashMap<>());
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithNonRestApiConsumerFactoryComponentThrowsException() throws Exception {
+ // Add a component that is not a RestApiConsumerFactory
+ TestNonRestApiComponent nonRestApiComponent = new TestNonRestApiComponent();
+ nonRestApiComponent.setCamelContext(camelContext);
+ camelContext.addComponent("non-api-component", nonRestApiComponent);
+
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:api-doc");
+ endpoint.setConsumerComponentName("non-api-component");
+ endpoint.setParameters(new HashMap<>());
+
+ assertThatThrownBy(() -> endpoint.createConsumer(exchange -> {
+ }))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("is not a RestApiConsumerFactory");
+ }
+
+ @Test
+ void testCreateConsumerWithNonExistentComponentThrowsNoSuchBeanException() throws Exception {
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:api-doc");
+ endpoint.setConsumerComponentName("nonexistent");
+ endpoint.setParameters(new HashMap<>());
+
+ assertThatThrownBy(() -> endpoint.createConsumer(exchange -> {
+ }))
+ .isInstanceOf(NoSuchBeanException.class);
+ }
+
+ @Test
+ void testCreateConsumerWithRestApiConsumerFactoryComponent() throws Exception {
+ // Create a component that implements RestApiConsumerFactory
+ RestApiConsumerFactory mockFactory = mock(RestApiConsumerFactory.class);
+ when(mockFactory.createApiConsumer(
+ any(CamelContext.class),
+ any(Processor.class),
+ eq("/api-doc"),
+ any(RestConfiguration.class),
+ any(Map.class))).thenReturn(mockConsumer);
+
+ // Create a component wrapper
+ TestRestApiComponent testComponent = new TestRestApiComponent(mockFactory);
+ testComponent.setCamelContext(camelContext);
+ camelContext.addComponent("test-api-component", testComponent);
+
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:api-doc");
+ endpoint.setConsumerComponentName("test-api-component");
+ endpoint.setParameters(new HashMap<>());
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithFactoryInRegistry() throws Exception {
+ // Register a RestApiConsumerFactory in registry (not as a component)
+ RestApiConsumerFactory mockFactory = mock(RestApiConsumerFactory.class);
+ when(mockFactory.createApiConsumer(
+ any(CamelContext.class),
+ any(Processor.class),
+ eq("/api-doc"),
+ any(RestConfiguration.class),
+ any(Map.class))).thenReturn(mockConsumer);
+
+ registry.bind("apiFactory", mockFactory);
+
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:api-doc");
+ endpoint.setParameters(new HashMap<>());
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithPathWithoutLeadingSlash() throws Exception {
+ RestApiConsumerFactory mockFactory = mock(RestApiConsumerFactory.class);
+ when(mockFactory.createApiConsumer(
+ any(CamelContext.class),
+ any(Processor.class),
+ eq("/openapi"),
+ any(RestConfiguration.class),
+ any(Map.class))).thenReturn(mockConsumer);
+
+ registry.bind("apiFactory", mockFactory);
+
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:openapi");
+ endpoint.setParameters(new HashMap<>());
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateProducerWithFactoryInRegistry() throws Exception {
+ RestApiProcessorFactory mockFactory = mock(RestApiProcessorFactory.class);
+ Processor mockApiProcessor = mock(Processor.class);
+ when(mockFactory.createApiProcessor(
+ any(CamelContext.class),
+ eq("/api-doc"),
+ any(RestConfiguration.class),
+ any(Map.class))).thenReturn(mockApiProcessor);
+
+ registry.bind("apiProcessorFactory", mockFactory);
+
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:api-doc");
+ endpoint.setParameters(new HashMap<>());
+
+ var producer = endpoint.createProducer();
+ assertThat(producer).isNotNull();
+ assertThat(producer).isInstanceOf(RestApiProducer.class);
+ }
+
+ @Test
+ void testCreateProducerWithApiComponentNameFromConfig() throws Exception {
+ RestApiProcessorFactory mockFactory = mock(RestApiProcessorFactory.class);
+ Processor mockApiProcessor = mock(Processor.class);
+ when(mockFactory.createApiProcessor(
+ any(CamelContext.class),
+ eq("/swagger"),
+ any(RestConfiguration.class),
+ any(Map.class))).thenReturn(mockApiProcessor);
+
+ registry.bind("apiProcessorFactory", mockFactory);
+
+ RestConfiguration config = new RestConfiguration();
+ config.setApiComponent("customApi");
+ camelContext.setRestConfiguration(config);
+
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:swagger");
+ endpoint.setParameters(new HashMap<>());
+
+ var producer = endpoint.createProducer();
+ assertThat(producer).isNotNull();
+ }
+
+ /**
+ * Test component that implements RestApiConsumerFactory
+ */
+ private static class TestRestApiComponent extends DefaultComponent implements RestApiConsumerFactory {
+
+ private final RestApiConsumerFactory delegate;
+
+ TestRestApiComponent(RestApiConsumerFactory delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Consumer createApiConsumer(
+ CamelContext camelContext, Processor processor, String contextPath,
+ RestConfiguration configuration, Map parameters)
+ throws Exception {
+ return delegate.createApiConsumer(camelContext, processor, contextPath, configuration, parameters);
+ }
+
+ @Override
+ protected org.apache.camel.Endpoint createEndpoint(String uri, String remaining, Map parameters) {
+ return null;
+ }
+ }
+
+ /**
+ * Test component that does NOT implement RestApiConsumerFactory
+ */
+ private static class TestNonRestApiComponent extends DefaultComponent {
+
+ @Override
+ protected org.apache.camel.Endpoint createEndpoint(String uri, String remaining, Map parameters) {
+ return null;
+ }
+ }
+}
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestApiEndpointTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestApiEndpointTest.java
new file mode 100644
index 0000000000000..adf28eec9db2e
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestApiEndpointTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.rest;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ExchangePattern;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for RestApiEndpoint focusing on behavior and error handling.
+ */
+class RestApiEndpointTest {
+
+ private CamelContext camelContext;
+ private RestApiComponent component;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ component = new RestApiComponent();
+ component.setCamelContext(camelContext);
+ camelContext.addComponent("rest-api", component);
+ camelContext.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ camelContext.stop();
+ }
+
+ @Test
+ void testDefaultExchangePattern() throws Exception {
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:api-doc");
+ assertThat(endpoint.getExchangePattern()).isEqualTo(ExchangePattern.InOut);
+ }
+
+ @Test
+ void testIsNotRemote() throws Exception {
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:api-doc");
+ assertThat(endpoint.isRemote()).isFalse();
+ }
+
+ @Test
+ void testIsLenientProperties() throws Exception {
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:api-doc");
+ assertThat(endpoint.isLenientProperties()).isTrue();
+ }
+
+ @Test
+ void testCreateProducerWithoutFactory() throws Exception {
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:api-doc");
+
+ assertThatThrownBy(endpoint::createProducer)
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Cannot find RestApiProcessorFactory");
+ }
+
+ @Test
+ void testCreateConsumerWithoutFactory() throws Exception {
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:api-doc");
+
+ assertThatThrownBy(() -> endpoint.createConsumer(exchange -> {
+ }))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Cannot find RestApiConsumerFactory");
+ }
+
+ @Test
+ void testPathWithLeadingSlash() throws Exception {
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:/api-doc");
+ assertThat(endpoint.getPath()).isEqualTo("/api-doc");
+ }
+
+ @Test
+ void testPathWithNestedPath() throws Exception {
+ RestApiEndpoint endpoint = (RestApiEndpoint) camelContext.getEndpoint("rest-api:api/v2/doc");
+ assertThat(endpoint.getPath()).isEqualTo("api/v2/doc");
+ }
+}
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestComponentTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestComponentTest.java
new file mode 100644
index 0000000000000..dbe30efb951b1
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestComponentTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.rest;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for RestComponent focusing on URI parsing and endpoint creation behavior.
+ */
+class RestComponentTest {
+
+ private CamelContext camelContext;
+ private RestComponent component;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ component = new RestComponent();
+ component.setCamelContext(camelContext);
+ camelContext.addComponent("rest", component);
+ camelContext.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ camelContext.stop();
+ }
+
+ // ==================== URI Parsing Tests ====================
+
+ @Test
+ void testCreateEndpointParsesMethodAndPath() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest:get:users?host=localhost:8080");
+
+ assertThat(endpoint).isInstanceOf(RestEndpoint.class);
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getMethod()).isEqualTo("get");
+ assertThat(restEndpoint.getPath()).isEqualTo("users");
+ assertThat(restEndpoint.getUriTemplate()).isNull();
+ }
+
+ @Test
+ void testCreateEndpointParsesMethodPathAndUriTemplate() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest:get:users:{id}?host=localhost:8080");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getMethod()).isEqualTo("get");
+ assertThat(restEndpoint.getPath()).isEqualTo("users");
+ assertThat(restEndpoint.getUriTemplate()).isEqualTo("{id}");
+ }
+
+ @Test
+ void testCreateEndpointSupportsAllHttpMethods() throws Exception {
+ String[] methods = { "get", "post", "put", "delete", "patch", "head", "options" };
+
+ for (String method : methods) {
+ Endpoint endpoint = camelContext.getEndpoint("rest:" + method + ":test?host=localhost");
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getMethod()).isEqualTo(method);
+ }
+ }
+
+ @Test
+ void testCreateEndpointThrowsExceptionForInvalidSyntax() {
+ assertThatThrownBy(() -> camelContext.getEndpoint("rest:get"))
+ .hasMessageContaining("Invalid syntax");
+ }
+
+ @Test
+ void testCreateEndpointStripsTrailingSlashFromPath() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest:get:users/?host=localhost");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getPath()).isEqualTo("users");
+ }
+
+ // ==================== Host Handling Tests ====================
+
+ @Test
+ void testCreateEndpointAddsHttpPrefixWhenMissing() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest:get:users?host=localhost:8080");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getHost()).isEqualTo("http://localhost:8080");
+ }
+
+ @Test
+ void testCreateEndpointPreservesHttpScheme() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest:get:users?host=http://localhost:8080");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getHost()).isEqualTo("http://localhost:8080");
+ }
+
+ @Test
+ void testCreateEndpointPreservesHttpsScheme() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest:get:users?host=https://localhost:8443");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getHost()).isEqualTo("https://localhost:8443");
+ }
+
+ // ==================== Component Property Propagation Tests ====================
+
+ @Test
+ void testComponentConsumerNamePropagatestoEndpoint() throws Exception {
+ component.setConsumerComponentName("servlet");
+ Endpoint endpoint = camelContext.getEndpoint("rest:get:users?host=localhost");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getConsumerComponentName()).isEqualTo("servlet");
+ }
+
+ @Test
+ void testComponentProducerNamePropagatestoEndpoint() throws Exception {
+ component.setProducerComponentName("undertow");
+ Endpoint endpoint = camelContext.getEndpoint("rest:get:users?host=localhost");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getProducerComponentName()).isEqualTo("undertow");
+ }
+
+ @Test
+ void testComponentApiDocPropagatestoEndpoint() throws Exception {
+ component.setApiDoc("swagger.json");
+ Endpoint endpoint = camelContext.getEndpoint("rest:get:users?host=localhost");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getApiDoc()).isEqualTo("swagger.json");
+ }
+
+ // ==================== Endpoint Options Tests ====================
+
+ @Test
+ void testCreateEndpointWithConsumesOption() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest:post:users?host=localhost&consumes=application/json");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getConsumes()).isEqualTo("application/json");
+ }
+
+ @Test
+ void testCreateEndpointWithProducesOption() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest:get:users?host=localhost&produces=application/xml");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getProduces()).isEqualTo("application/xml");
+ }
+
+ @Test
+ void testCreateEndpointWithBindingModeOption() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint("rest:get:users?host=localhost&bindingMode=json");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getBindingMode()).isNotNull();
+ }
+
+ @Test
+ void testCreateEndpointWithInTypeAndOutType() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint(
+ "rest:post:users?host=localhost&inType=com.example.User&outType=com.example.UserResponse");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getInType()).isEqualTo("com.example.User");
+ assertThat(restEndpoint.getOutType()).isEqualTo("com.example.UserResponse");
+ }
+
+ @Test
+ void testCreateEndpointWithComponentNames() throws Exception {
+ Endpoint endpoint = camelContext.getEndpoint(
+ "rest:get:users?host=localhost&consumerComponentName=jetty&producerComponentName=http");
+
+ RestEndpoint restEndpoint = (RestEndpoint) endpoint;
+ assertThat(restEndpoint.getConsumerComponentName()).isEqualTo("jetty");
+ assertThat(restEndpoint.getProducerComponentName()).isEqualTo("http");
+ }
+}
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestConfigurerTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestConfigurerTest.java
new file mode 100644
index 0000000000000..e7b09b90530ad
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestConfigurerTest.java
@@ -0,0 +1,193 @@
+/*
+ * 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.rest;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class RestConfigurerTest {
+
+ private CamelContext camelContext;
+ private RestComponent component;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ component = new RestComponent();
+ component.setCamelContext(camelContext);
+ camelContext.addComponent("rest", component);
+ camelContext.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ camelContext.stop();
+ }
+
+ @Test
+ void testRestComponentConfigurerGetOptionType() {
+ RestComponentConfigurer configurer = new RestComponentConfigurer();
+
+ assertThat(configurer.getOptionType("consumerComponentName", false)).isEqualTo(String.class);
+ assertThat(configurer.getOptionType("producerComponentName", false)).isEqualTo(String.class);
+ assertThat(configurer.getOptionType("apiDoc", false)).isEqualTo(String.class);
+ assertThat(configurer.getOptionType("host", false)).isEqualTo(String.class);
+ assertThat(configurer.getOptionType("nonexistent", false)).isNull();
+ }
+
+ @Test
+ void testRestComponentConfigurerCaseInsensitive() {
+ RestComponentConfigurer configurer = new RestComponentConfigurer();
+
+ assertThat(configurer.getOptionType("CONSUMERCOMPONENTNAME", true)).isEqualTo(String.class);
+ assertThat(configurer.getOptionType("APIDOC", true)).isEqualTo(String.class);
+ }
+
+ @Test
+ void testRestComponentConfigurerConfigure() {
+ RestComponentConfigurer configurer = new RestComponentConfigurer();
+
+ assertThat(configurer.configure(camelContext, component, "consumerComponentName", "jetty", false)).isTrue();
+ assertThat(component.getConsumerComponentName()).isEqualTo("jetty");
+
+ assertThat(configurer.configure(camelContext, component, "producerComponentName", "http", false)).isTrue();
+ assertThat(component.getProducerComponentName()).isEqualTo("http");
+
+ assertThat(configurer.configure(camelContext, component, "apiDoc", "swagger.json", false)).isTrue();
+ assertThat(component.getApiDoc()).isEqualTo("swagger.json");
+
+ assertThat(configurer.configure(camelContext, component, "host", "localhost:8080", false)).isTrue();
+ assertThat(component.getHost()).isEqualTo("localhost:8080");
+
+ assertThat(configurer.configure(camelContext, component, "nonexistent", "value", false)).isFalse();
+ }
+
+ @Test
+ void testRestComponentConfigurerGetOptionValue() {
+ RestComponentConfigurer configurer = new RestComponentConfigurer();
+
+ component.setConsumerComponentName("jetty");
+ component.setProducerComponentName("http");
+ component.setApiDoc("openapi.json");
+ component.setHost("localhost:9090");
+
+ assertThat(configurer.getOptionValue(component, "consumerComponentName", false)).isEqualTo("jetty");
+ assertThat(configurer.getOptionValue(component, "producerComponentName", false)).isEqualTo("http");
+ assertThat(configurer.getOptionValue(component, "apiDoc", false)).isEqualTo("openapi.json");
+ assertThat(configurer.getOptionValue(component, "host", false)).isEqualTo("localhost:9090");
+ assertThat(configurer.getOptionValue(component, "nonexistent", false)).isNull();
+ }
+
+ @Test
+ void testRestEndpointConfigurerGetOptionType() throws Exception {
+ RestEndpointConfigurer configurer = new RestEndpointConfigurer();
+
+ assertThat(configurer.getOptionType("consumes", false)).isEqualTo(String.class);
+ assertThat(configurer.getOptionType("produces", false)).isEqualTo(String.class);
+ assertThat(configurer.getOptionType("host", false)).isEqualTo(String.class);
+ assertThat(configurer.getOptionType("routeId", false)).isEqualTo(String.class);
+ assertThat(configurer.getOptionType("description", false)).isEqualTo(String.class);
+ assertThat(configurer.getOptionType("nonexistent", false)).isNull();
+ }
+
+ @Test
+ void testRestEndpointConfigurerConfigure() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ RestEndpointConfigurer configurer = new RestEndpointConfigurer();
+
+ assertThat(configurer.configure(camelContext, endpoint, "consumes", "application/json", false)).isTrue();
+ assertThat(endpoint.getConsumes()).isEqualTo("application/json");
+
+ assertThat(configurer.configure(camelContext, endpoint, "produces", "application/xml", false)).isTrue();
+ assertThat(endpoint.getProduces()).isEqualTo("application/xml");
+
+ assertThat(configurer.configure(camelContext, endpoint, "routeId", "myRoute", false)).isTrue();
+ assertThat(endpoint.getRouteId()).isEqualTo("myRoute");
+
+ assertThat(configurer.configure(camelContext, endpoint, "description", "Test description", false)).isTrue();
+ assertThat(endpoint.getDescription()).isEqualTo("Test description");
+
+ assertThat(configurer.configure(camelContext, endpoint, "inType", "com.example.Input", false)).isTrue();
+ assertThat(endpoint.getInType()).isEqualTo("com.example.Input");
+
+ assertThat(configurer.configure(camelContext, endpoint, "outType", "com.example.Output", false)).isTrue();
+ assertThat(endpoint.getOutType()).isEqualTo("com.example.Output");
+
+ assertThat(configurer.configure(camelContext, endpoint, "nonexistent", "value", false)).isFalse();
+ }
+
+ @Test
+ void testRestEndpointConfigurerGetOptionValue() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ RestEndpointConfigurer configurer = new RestEndpointConfigurer();
+
+ endpoint.setConsumes("application/json");
+ endpoint.setProduces("application/xml");
+ endpoint.setRouteId("testRoute");
+ endpoint.setDescription("Test endpoint");
+
+ assertThat(configurer.getOptionValue(endpoint, "consumes", false)).isEqualTo("application/json");
+ assertThat(configurer.getOptionValue(endpoint, "produces", false)).isEqualTo("application/xml");
+ assertThat(configurer.getOptionValue(endpoint, "routeId", false)).isEqualTo("testRoute");
+ assertThat(configurer.getOptionValue(endpoint, "description", false)).isEqualTo("Test endpoint");
+ assertThat(configurer.getOptionValue(endpoint, "nonexistent", false)).isNull();
+ }
+
+ @Test
+ void testRestEndpointConfigurerConfigureBindingMode() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ RestEndpointConfigurer configurer = new RestEndpointConfigurer();
+
+ assertThat(configurer.configure(camelContext, endpoint, "bindingMode", "json", false)).isTrue();
+ assertThat(endpoint.getBindingMode()).isNotNull();
+ }
+
+ @Test
+ void testRestEndpointConfigurerConfigureApiDoc() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ RestEndpointConfigurer configurer = new RestEndpointConfigurer();
+
+ assertThat(configurer.configure(camelContext, endpoint, "apiDoc", "openapi.yaml", false)).isTrue();
+ assertThat(endpoint.getApiDoc()).isEqualTo("openapi.yaml");
+ }
+
+ @Test
+ void testRestEndpointConfigurerConfigureQueryParameters() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ RestEndpointConfigurer configurer = new RestEndpointConfigurer();
+
+ assertThat(configurer.configure(camelContext, endpoint, "queryParameters", "page=1&size=10", false)).isTrue();
+ assertThat(endpoint.getQueryParameters()).isEqualTo("page=1&size=10");
+ }
+
+ @Test
+ void testRestEndpointConfigurerConfigureComponentNames() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ RestEndpointConfigurer configurer = new RestEndpointConfigurer();
+
+ assertThat(configurer.configure(camelContext, endpoint, "consumerComponentName", "undertow", false)).isTrue();
+ assertThat(endpoint.getConsumerComponentName()).isEqualTo("undertow");
+
+ assertThat(configurer.configure(camelContext, endpoint, "producerComponentName", "vertx-http", false)).isTrue();
+ assertThat(endpoint.getProducerComponentName()).isEqualTo("vertx-http");
+ }
+}
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestEndpointProducerConsumerTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestEndpointProducerConsumerTest.java
new file mode 100644
index 0000000000000..eeee8c86fa5c7
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestEndpointProducerConsumerTest.java
@@ -0,0 +1,394 @@
+/*
+ * 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.rest;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Consumer;
+import org.apache.camel.Producer;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.RestConfiguration;
+import org.apache.camel.spi.RestConsumerFactory;
+import org.apache.camel.spi.RestProducerFactory;
+import org.apache.camel.support.SimpleRegistry;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class RestEndpointProducerConsumerTest {
+
+ private CamelContext camelContext;
+ private SimpleRegistry registry;
+ private RestComponent component;
+
+ @Mock
+ private RestProducerFactory mockProducerFactory;
+
+ @Mock
+ private RestConsumerFactory mockConsumerFactory;
+
+ @Mock
+ private Producer mockProducer;
+
+ @Mock
+ private Consumer mockConsumer;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ registry = new SimpleRegistry();
+ camelContext = new DefaultCamelContext(registry);
+ component = new RestComponent();
+ component.setCamelContext(camelContext);
+ camelContext.addComponent("rest", component);
+ camelContext.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ camelContext.stop();
+ }
+
+ @Test
+ void testCreateProducerWithRegisteredFactory() throws Exception {
+ registry.bind("myProducerFactory", mockProducerFactory);
+
+ when(mockProducerFactory.createProducer(
+ any(CamelContext.class), eq("http://localhost:8080"), eq("get"),
+ eq("users"), any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockProducer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost:8080&producerComponentName=myProducerFactory");
+
+ Producer producer = endpoint.createProducer();
+
+ assertThat(producer).isNotNull();
+ assertThat(producer).isInstanceOf(RestProducer.class);
+ }
+
+ @Test
+ void testCreateProducerWithNonRestProducerFactoryComponent() throws Exception {
+ registry.bind("myComponent", new Object());
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost:8080&producerComponentName=myComponent");
+
+ assertThatThrownBy(endpoint::createProducer)
+ .hasMessageContaining("RestProducerFactory");
+ }
+
+ @Test
+ void testCreateConsumerWithRegisteredFactory() throws Exception {
+ registry.bind("myConsumerFactory", mockConsumerFactory);
+
+ when(mockConsumerFactory.createConsumer(
+ any(CamelContext.class), any(), any(),
+ any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockConsumer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost:8080&consumerComponentName=myConsumerFactory");
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithNonRestConsumerFactoryComponent() throws Exception {
+ registry.bind("myComponent", new Object());
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost:8080&consumerComponentName=myComponent");
+
+ assertThatThrownBy(() -> endpoint.createConsumer(exchange -> {
+ }))
+ .hasMessageContaining("RestConsumerFactory");
+ }
+
+ @Test
+ void testProducerWithConsumerFallback() throws Exception {
+ // Register a factory that implements both producer and consumer
+ RestProducerFactory dualFactory = mock(RestProducerFactory.class);
+ registry.bind("dualFactory", dualFactory);
+
+ when(dualFactory.createProducer(
+ any(CamelContext.class), any(), any(),
+ any(), any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockProducer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost:8080&consumerComponentName=dualFactory");
+
+ // With no explicit producer component, it should fall back to consumer component
+ Producer producer = endpoint.createProducer();
+
+ assertThat(producer).isNotNull();
+ }
+
+ @Test
+ void testCreateProducerWithEmptyHost() throws Exception {
+ RestEndpoint endpoint = new RestEndpoint("rest:get:users", component);
+ endpoint.setMethod("get");
+ endpoint.setPath("users");
+ endpoint.setHost("");
+ endpoint.setParameters(new HashMap<>());
+
+ assertThatThrownBy(endpoint::createProducer)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Hostname must be configured");
+ }
+
+ @Test
+ void testCreateConsumerWithRestConfiguration() throws Exception {
+ RestConfiguration restConfig = new RestConfiguration();
+ restConfig.setHost("localhost");
+ restConfig.setPort(9090);
+ restConfig.setScheme("https");
+ restConfig.setContextPath("/api");
+ camelContext.setRestConfiguration(restConfig);
+
+ registry.bind("myConsumerFactory", mockConsumerFactory);
+
+ when(mockConsumerFactory.createConsumer(
+ any(CamelContext.class), any(), any(),
+ any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockConsumer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost&consumerComponentName=myConsumerFactory");
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithHostNameResolver() throws Exception {
+ RestConfiguration restConfig = new RestConfiguration();
+ restConfig.setHostNameResolver(RestConfiguration.RestHostNameResolver.localIp);
+ camelContext.setRestConfiguration(restConfig);
+
+ registry.bind("myConsumerFactory", mockConsumerFactory);
+
+ when(mockConsumerFactory.createConsumer(
+ any(CamelContext.class), any(), any(),
+ any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockConsumer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost&consumerComponentName=myConsumerFactory");
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithAllLocalIpResolver() throws Exception {
+ RestConfiguration restConfig = new RestConfiguration();
+ restConfig.setHostNameResolver(RestConfiguration.RestHostNameResolver.allLocalIp);
+ camelContext.setRestConfiguration(restConfig);
+
+ registry.bind("myConsumerFactory", mockConsumerFactory);
+
+ when(mockConsumerFactory.createConsumer(
+ any(CamelContext.class), any(), any(),
+ any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockConsumer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost&consumerComponentName=myConsumerFactory");
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithLocalHostNameResolver() throws Exception {
+ RestConfiguration restConfig = new RestConfiguration();
+ restConfig.setHostNameResolver(RestConfiguration.RestHostNameResolver.localHostName);
+ camelContext.setRestConfiguration(restConfig);
+
+ registry.bind("myConsumerFactory", mockConsumerFactory);
+
+ when(mockConsumerFactory.createConsumer(
+ any(CamelContext.class), any(), any(),
+ any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockConsumer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost&consumerComponentName=myConsumerFactory");
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithUriTemplateStartingWithSlash() throws Exception {
+ registry.bind("myConsumerFactory", mockConsumerFactory);
+
+ when(mockConsumerFactory.createConsumer(
+ any(CamelContext.class), any(), any(),
+ any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockConsumer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users:/{id}?host=localhost&consumerComponentName=myConsumerFactory");
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithPathNotStartingWithSlash() throws Exception {
+ registry.bind("myConsumerFactory", mockConsumerFactory);
+
+ when(mockConsumerFactory.createConsumer(
+ any(CamelContext.class), any(), any(),
+ any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockConsumer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost&consumerComponentName=myConsumerFactory");
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithContextPathStartingWithSlash() throws Exception {
+ RestConfiguration restConfig = new RestConfiguration();
+ restConfig.setContextPath("/api/v1");
+ camelContext.setRestConfiguration(restConfig);
+
+ registry.bind("myConsumerFactory", mockConsumerFactory);
+
+ when(mockConsumerFactory.createConsumer(
+ any(CamelContext.class), any(), any(),
+ any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockConsumer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost&consumerComponentName=myConsumerFactory");
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithContextPathNotStartingWithSlash() throws Exception {
+ RestConfiguration restConfig = new RestConfiguration();
+ restConfig.setContextPath("api/v1");
+ camelContext.setRestConfiguration(restConfig);
+
+ registry.bind("myConsumerFactory", mockConsumerFactory);
+
+ when(mockConsumerFactory.createConsumer(
+ any(CamelContext.class), any(), any(),
+ any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockConsumer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost&consumerComponentName=myConsumerFactory");
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithNonDefaultPort() throws Exception {
+ RestConfiguration restConfig = new RestConfiguration();
+ restConfig.setPort(9090);
+ camelContext.setRestConfiguration(restConfig);
+
+ registry.bind("myConsumerFactory", mockConsumerFactory);
+
+ when(mockConsumerFactory.createConsumer(
+ any(CamelContext.class), any(), any(),
+ any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockConsumer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users?host=localhost&consumerComponentName=myConsumerFactory");
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+
+ assertThat(consumer).isNotNull();
+ }
+
+ @Test
+ void testCreateConsumerWithUriTemplate() throws Exception {
+ registry.bind("myConsumerFactory", mockConsumerFactory);
+
+ when(mockConsumerFactory.createConsumer(
+ any(CamelContext.class), any(), any(),
+ any(), any(), any(), any(),
+ any(RestConfiguration.class), any(Map.class)))
+ .thenReturn(mockConsumer);
+
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint(
+ "rest:get:users:{id}?host=localhost&consumerComponentName=myConsumerFactory");
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+
+ assertThat(consumer).isNotNull();
+ assertThat(endpoint.getUriTemplate()).isEqualTo("{id}");
+ }
+}
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestEndpointTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestEndpointTest.java
new file mode 100644
index 0000000000000..e98ce4f6de332
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestEndpointTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.rest;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.RestConfiguration;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for RestEndpoint focusing on behavior and error handling.
+ */
+class RestEndpointTest {
+
+ private CamelContext camelContext;
+ private RestComponent component;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ component = new RestComponent();
+ component.setCamelContext(camelContext);
+ camelContext.addComponent("rest", component);
+ camelContext.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ camelContext.stop();
+ }
+
+ @Test
+ void testCreateProducerWithoutHostThrowsIllegalArgumentException() throws Exception {
+ RestEndpoint endpoint = new RestEndpoint("rest:get:users", component);
+ endpoint.setMethod("get");
+ endpoint.setPath("users");
+ endpoint.setParameters(new HashMap<>());
+
+ assertThatThrownBy(endpoint::createProducer)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Hostname must be configured");
+ }
+
+ @Test
+ void testCreateConsumerWithoutFactoryThrowsIllegalStateException() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+
+ assertThatThrownBy(() -> endpoint.createConsumer(exchange -> {
+ }))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("Cannot find RestConsumerFactory");
+ }
+
+ @Test
+ void testConfigurePropertiesMergesParametersMap() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+
+ Map options = new HashMap<>();
+ Map params = new HashMap<>();
+ params.put("customKey", "customValue");
+ options.put("parameters", params);
+
+ endpoint.configureProperties(options);
+
+ assertThat(endpoint.getParameters()).containsEntry("customKey", "customValue");
+ }
+
+ @Test
+ void testBindingModeCanBeSetFromString() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost&bindingMode=auto");
+
+ assertThat(endpoint.getBindingMode()).isEqualTo(RestConfiguration.RestBindingMode.auto);
+ }
+
+ @Test
+ void testBindingModeCanBeSetFromEnum() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+
+ endpoint.setBindingMode(RestConfiguration.RestBindingMode.json);
+ assertThat(endpoint.getBindingMode()).isEqualTo(RestConfiguration.RestBindingMode.json);
+
+ endpoint.setBindingMode("xml");
+ assertThat(endpoint.getBindingMode()).isEqualTo(RestConfiguration.RestBindingMode.xml);
+ }
+
+ @Test
+ void testHostWithoutSchemeGetsHttpPrefixAdded() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost:8080");
+
+ assertThat(endpoint.getHost()).isEqualTo("http://localhost:8080");
+ }
+
+ @Test
+ void testHostWithHttpSchemeIsPreserved() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=http://localhost:8080");
+
+ assertThat(endpoint.getHost()).isEqualTo("http://localhost:8080");
+ }
+
+ @Test
+ void testHostWithHttpsSchemeIsPreserved() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=https://localhost:8443");
+
+ assertThat(endpoint.getHost()).isEqualTo("https://localhost:8443");
+ }
+
+ @Test
+ void testIsLenientPropertiesReturnsTrue() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+
+ assertThat(endpoint.isLenientProperties()).isTrue();
+ }
+}
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestProducerAdvancedTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestProducerAdvancedTest.java
new file mode 100644
index 0000000000000..9680e1d5d00ed
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestProducerAdvancedTest.java
@@ -0,0 +1,334 @@
+/*
+ * 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.rest;
+
+import java.util.HashMap;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.RestConfiguration;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+class RestProducerAdvancedTest {
+
+ private CamelContext camelContext;
+ private RestComponent component;
+
+ @Mock
+ private Producer mockProducer;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ component = new RestComponent();
+ component.setCamelContext(camelContext);
+ camelContext.addComponent("rest", component);
+ camelContext.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ camelContext.stop();
+ }
+
+ @Test
+ void testPrepareExchangeWithMultiplePathPlaceholders() throws Exception {
+ // The colon is used in the URI syntax as path:uriTemplate, so we test with slashes
+ RestEndpoint endpoint
+ = (RestEndpoint) camelContext.getEndpoint("rest:get:users/{userId}/orders/{orderId}?host=http://localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("userId", "123");
+ exchange.getMessage().setHeader("orderId", "456");
+ producer.prepareExchange(exchange);
+
+ String uri = exchange.getMessage().getHeader(RestConstants.REST_HTTP_URI, String.class);
+ assertThat(uri).isEqualTo("http://localhost/users/123/orders/456");
+ }
+
+ @Test
+ void testPrepareExchangeWithUnresolvedPlaceholder() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users:{userId}?host=http://localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ // Don't set the header, so placeholder won't be resolved
+ producer.prepareExchange(exchange);
+
+ // When placeholder is not resolved, REST_HTTP_URI should not be set
+ String uri = exchange.getMessage().getHeader(RestConstants.REST_HTTP_URI, String.class);
+ assertThat(uri).isNull();
+ }
+
+ @Test
+ void testPrepareExchangeWithMalformedPlaceholder() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users:{userId?host=http://localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("userId", "123");
+ producer.prepareExchange(exchange);
+
+ // Malformed placeholder with unclosed brace should not crash
+ assertThat(exchange.getException()).isNull();
+ }
+
+ @Test
+ void testPrepareExchangeWithPathOnly() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=http://localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ producer.prepareExchange(exchange);
+
+ // No template resolution needed, so no REST_HTTP_URI
+ String uri = exchange.getMessage().getHeader(RestConstants.REST_HTTP_URI, String.class);
+ assertThat(uri).isNull();
+ }
+
+ @Test
+ void testPrepareExchangeWithBasePathAndTemplate() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:api/v1:users/{id}?host=http://localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("id", "999");
+ producer.prepareExchange(exchange);
+
+ String uri = exchange.getMessage().getHeader(RestConstants.REST_HTTP_URI, String.class);
+ assertThat(uri).isEqualTo("http://localhost/api/v1/users/999");
+ }
+
+ @Test
+ void testPrepareExchangeWithEmptyQueryParameters() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ endpoint.setQueryParameters("");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ producer.prepareExchange(exchange);
+
+ String query = exchange.getMessage().getHeader(RestConstants.REST_HTTP_QUERY, String.class);
+ // Empty query parameters result in empty string, not null
+ assertThat(query).isEmpty();
+ }
+
+ @Test
+ void testPrepareExchangePreservesExistingHeaders() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:post:users?host=localhost");
+ endpoint.setProduces("application/json");
+ endpoint.setConsumes("application/xml");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("Custom-Header", "custom-value");
+ producer.prepareExchange(exchange);
+
+ assertThat(exchange.getMessage().getHeader("Custom-Header")).isEqualTo("custom-value");
+ assertThat(exchange.getMessage().getHeader(RestConstants.HTTP_METHOD)).isEqualTo("POST");
+ }
+
+ @Test
+ void testPrepareExchangeWithAllMethodTypes() throws Exception {
+ String[] methods = { "get", "post", "put", "delete", "patch", "head", "options", "trace", "connect" };
+
+ for (String method : methods) {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:" + method + ":test?host=localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ producer.prepareExchange(exchange);
+
+ assertThat(exchange.getMessage().getHeader(RestConstants.HTTP_METHOD))
+ .isEqualTo(method.toUpperCase());
+ }
+ }
+
+ @Test
+ void testPrepareExchangeWithNullMethod() throws Exception {
+ RestEndpoint endpoint = new RestEndpoint("rest:null:test", component);
+ endpoint.setPath("test");
+ endpoint.setMethod(null);
+ endpoint.setHost("http://localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ producer.prepareExchange(exchange);
+
+ // Method is null, so HTTP_METHOD should not be set
+ assertThat(exchange.getMessage().getHeader(RestConstants.HTTP_METHOD)).isNull();
+ }
+
+ @Test
+ void testPrepareExchangeWithNullProducesAndConsumes() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ endpoint.setProduces(null);
+ endpoint.setConsumes(null);
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ producer.prepareExchange(exchange);
+
+ // No content-type or accept headers should be set
+ assertThat(exchange.getMessage().getHeader(RestConstants.CONTENT_TYPE)).isNull();
+ assertThat(exchange.getMessage().getHeader(RestConstants.ACCEPT)).isNull();
+ }
+
+ @Test
+ void testProcessWithExceptionDuringPrepare() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ // Set query parameters that will cause an exception during parsing
+ endpoint.setQueryParameters("invalid=%%");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ boolean[] callbackDone = { false };
+
+ boolean result = producer.process(exchange, doneSync -> callbackDone[0] = true);
+
+ assertThat(result).isTrue();
+ assertThat(callbackDone[0]).isTrue();
+ assertThat(exchange.getException()).isNotNull();
+ }
+
+ @Test
+ void testRestProducerLifecycle() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ config.setBindingMode(RestConfiguration.RestBindingMode.off);
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ // Test lifecycle
+ producer.doInit();
+ producer.doStart();
+
+ assertThat(producer.getEndpoint()).isSameAs(endpoint);
+
+ producer.doStop();
+ }
+
+ @Test
+ void testCreateQueryParametersWithTrailingAmpersand() throws Exception {
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("a", "1");
+ // b is optional and not set
+
+ String result = RestProducer.createQueryParameters("a={a}&b={b?}", exchange);
+
+ assertThat(result).isEqualTo("a=1");
+ assertThat(result).doesNotEndWith("&");
+ }
+
+ @Test
+ void testCreateQueryParametersWithAllOptionalMissing() throws Exception {
+ Exchange exchange = new DefaultExchange(camelContext);
+ // Don't set any headers
+
+ String result = RestProducer.createQueryParameters("a={a?}&b={b?}&c={c?}", exchange);
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ void testCreateQueryParametersWithMixedValues() throws Exception {
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("a", "value1");
+ exchange.setVariable("c", "value3");
+ // b is optional and not set
+
+ String result = RestProducer.createQueryParameters("a={a}&b={b?}&c={c}", exchange);
+
+ assertThat(result).contains("a=value1");
+ assertThat(result).contains("c=value3");
+ assertThat(result).doesNotContain("b=");
+ }
+
+ @Test
+ void testCreateQueryParametersWithSpecialCharacters() throws Exception {
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("name", "John Doe");
+ exchange.getMessage().setHeader("email", "john@example.com");
+
+ String result = RestProducer.createQueryParameters("name={name}&email={email}", exchange);
+
+ assertThat(result).contains("name=John+Doe");
+ assertThat(result).contains("email=john%40example.com");
+ }
+
+ @Test
+ void testPrepareExchangeWithLeadingSlashInUriTemplate() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:api:/{id}?host=http://localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("id", "123");
+ producer.prepareExchange(exchange);
+
+ String uri = exchange.getMessage().getHeader(RestConstants.REST_HTTP_URI, String.class);
+ assertThat(uri).isEqualTo("http://localhost/api/123");
+ }
+}
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestProducerBindingCallbackTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestProducerBindingCallbackTest.java
new file mode 100644
index 0000000000000..2347e02ece731
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestProducerBindingCallbackTest.java
@@ -0,0 +1,402 @@
+/*
+ * 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.rest;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.camel.AsyncCallback;
+import org.apache.camel.AsyncProcessor;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.DataFormat;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class RestProducerBindingCallbackTest {
+
+ private CamelContext camelContext;
+
+ @Mock
+ private AsyncProcessor mockProcessor;
+
+ @Mock
+ private DataFormat jsonDataFormat;
+
+ @Mock
+ private DataFormat xmlDataFormat;
+
+ @Mock
+ private DataFormat outJsonDataFormat;
+
+ @Mock
+ private DataFormat outXmlDataFormat;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ camelContext.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ camelContext.stop();
+ }
+
+ @Test
+ void testCallbackWithJsonResponse() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.getMessage().setBody("{\"result\": \"ok\"}");
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/json");
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ }
+
+ @Test
+ void testCallbackWithXmlResponse() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.getMessage().setBody("ok");
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/xml");
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "xml", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ }
+
+ @Test
+ void testCallbackSkipsBindingOnErrorCode() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.getMessage().setBody("Error");
+ exchange.getMessage().setHeader(RestConstants.HTTP_RESPONSE_CODE, 500);
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/json");
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ // Body should not have been transformed due to error code skip
+ assertThat(exchange.getMessage().getBody(String.class)).isEqualTo("Error");
+ }
+
+ @Test
+ void testCallbackWithEmptyResponseBody() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.getMessage().setBody(null);
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/json");
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ }
+
+ @Test
+ void testCallbackWithExceptionInExchange() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.setException(new RuntimeException("Test error"));
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ assertThat(exchange.getException()).isNotNull();
+ }
+
+ @Test
+ void testCallbackWithBindingModeOff() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.getMessage().setBody("{\"result\": \"ok\"}");
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/json");
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "off", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ }
+
+ @Test
+ void testCallbackWithBindingModeAuto() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.getMessage().setBody("{\"result\": \"ok\"}");
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/json");
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "auto", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ }
+
+ @Test
+ void testCallbackWithNoContentType() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.getMessage().setBody("{\"result\": \"ok\"}");
+ // No content type set
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ // Content-Type should be set by the callback
+ assertThat(exchange.getMessage().getHeader(RestConstants.CONTENT_TYPE)).isEqualTo("application/json");
+ }
+
+ @Test
+ void testCallbackWithXmlContentTypeWhenJsonMode() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.getMessage().setBody("ok");
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/xml");
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ }
+
+ @Test
+ void testCallbackWithJsonXmlBindingMode() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.getMessage().setBody("{\"result\": \"ok\"}");
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/json");
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json_xml", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ }
+
+ @Test
+ void testCallbackWith3xxResponseCode() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.getMessage().setBody("Redirect");
+ exchange.getMessage().setHeader(RestConstants.HTTP_RESPONSE_CODE, 302);
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/json");
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ }
+
+ @Test
+ void testCallbackWithNoUnmarshallers() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.getMessage().setBody("{\"result\": \"ok\"}");
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/json");
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, null, null,
+ null, null, "json", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ }
+
+ @Test
+ void testCallbackWithSkipBindingOnErrorCodeDisabled() {
+ AtomicBoolean callbackCalled = new AtomicBoolean(false);
+
+ doAnswer(invocation -> {
+ Exchange exchange = invocation.getArgument(0);
+ AsyncCallback callback = invocation.getArgument(1);
+ exchange.getMessage().setBody("Error");
+ exchange.getMessage().setHeader(RestConstants.HTTP_RESPONSE_CODE, 400);
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/json");
+ callback.done(true);
+ return true;
+ }).when(mockProcessor).process(any(Exchange.class), any(AsyncCallback.class));
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", false, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ processor.process(exchange, doneSync -> callbackCalled.set(true));
+
+ assertThat(callbackCalled.get()).isTrue();
+ }
+}
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestProducerBindingProcessorTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestProducerBindingProcessorTest.java
new file mode 100644
index 0000000000000..dc9f7a3e96594
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestProducerBindingProcessorTest.java
@@ -0,0 +1,311 @@
+/*
+ * 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.rest;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.AsyncCallback;
+import org.apache.camel.AsyncProcessor;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.DataFormat;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class RestProducerBindingProcessorTest {
+
+ private CamelContext camelContext;
+
+ @Mock
+ private AsyncProcessor mockProcessor;
+
+ @Mock
+ private DataFormat jsonDataFormat;
+
+ @Mock
+ private DataFormat xmlDataFormat;
+
+ @Mock
+ private DataFormat outJsonDataFormat;
+
+ @Mock
+ private DataFormat outXmlDataFormat;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ camelContext.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ camelContext.stop();
+ }
+
+ @Test
+ void testProcessWithEmptyBody() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, null);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithStringBody() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, null);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody("{\"name\": \"test\"}");
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithByteArrayBody() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, null);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody("{\"name\": \"test\"}".getBytes(StandardCharsets.UTF_8));
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithInputStreamBody() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, null);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(new ByteArrayInputStream("{\"name\": \"test\"}".getBytes(StandardCharsets.UTF_8)));
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithBindingModeOff() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, null, null, null, null, "off", true, null);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(new Object());
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithBindingModeAuto() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, null, null, null, null, "auto", true, null);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(new Object());
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithJsonContentType() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "auto", true, null);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody("test");
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/json");
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithXmlContentType() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "auto", true, null);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody("test");
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/xml");
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithOutType() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody(null);
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithSkipBindingOnErrorCodeEnabled() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", true, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody("test");
+ exchange.getMessage().setHeader(RestConstants.HTTP_RESPONSE_CODE, 500);
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithSkipBindingOnErrorCodeDisabled() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, xmlDataFormat,
+ outJsonDataFormat, outXmlDataFormat, "json", false, "com.example.Response");
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody("test");
+ exchange.getMessage().setHeader(RestConstants.HTTP_RESPONSE_CODE, 500);
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithNoDataFormats() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, null, null, null, null, "json", true, null);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody("plain text");
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithJsonBindingModeNoXml() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, jsonDataFormat, null,
+ outJsonDataFormat, null, "json", true, null);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody("test");
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/json");
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ void testProcessWithXmlBindingModeNoJson() {
+ when(mockProcessor.process(any(Exchange.class), any(AsyncCallback.class))).thenReturn(true);
+
+ RestProducerBindingProcessor processor = new RestProducerBindingProcessor(
+ mockProcessor, camelContext, null, xmlDataFormat,
+ null, outXmlDataFormat, "xml", true, null);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setBody("test");
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "application/xml");
+
+ boolean result = processor.process(exchange, doneSync -> {
+ });
+
+ assertThat(result).isTrue();
+ }
+}
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestProducerTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestProducerTest.java
new file mode 100644
index 0000000000000..46f4040287668
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestProducerTest.java
@@ -0,0 +1,354 @@
+/*
+ * 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.rest;
+
+import java.net.URISyntaxException;
+import java.util.HashMap;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.RestConfiguration;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for RestProducer focusing on query parameter resolution and exchange preparation.
+ */
+@ExtendWith(MockitoExtension.class)
+class RestProducerTest {
+
+ private CamelContext camelContext;
+ private RestComponent component;
+
+ @Mock
+ private Producer mockProducer;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ component = new RestComponent();
+ component.setCamelContext(camelContext);
+ camelContext.addComponent("rest", component);
+ camelContext.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ camelContext.stop();
+ }
+
+ // ==================== Query Parameter Resolution Tests ====================
+
+ @Test
+ void testCreateQueryParametersReturnsUnmodifiedQueryWhenNoPlaceholders() throws URISyntaxException {
+ Exchange exchange = new DefaultExchange(camelContext);
+
+ String result = RestProducer.createQueryParameters("page=1&size=10", exchange);
+
+ assertThat(result).isEqualTo("page=1&size=10");
+ }
+
+ @Test
+ void testCreateQueryParametersResolvesPlaceholderFromHeader() throws URISyntaxException {
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("userId", "123");
+
+ String result = RestProducer.createQueryParameters("id={userId}", exchange);
+
+ assertThat(result).isEqualTo("id=123");
+ }
+
+ @Test
+ void testCreateQueryParametersResolvesMultiplePlaceholders() throws URISyntaxException {
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("page", "2");
+ exchange.getMessage().setHeader("size", "50");
+
+ String result = RestProducer.createQueryParameters("page={page}&size={size}", exchange);
+
+ assertThat(result).isEqualTo("page=2&size=50");
+ }
+
+ @Test
+ void testCreateQueryParametersRemovesOptionalPlaceholderWhenNotSet() throws URISyntaxException {
+ Exchange exchange = new DefaultExchange(camelContext);
+
+ String result = RestProducer.createQueryParameters("filter={filter?}", exchange);
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ void testCreateQueryParametersResolvesOptionalPlaceholderWhenSet() throws URISyntaxException {
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("filter", "active");
+
+ String result = RestProducer.createQueryParameters("filter={filter?}", exchange);
+
+ assertThat(result).isEqualTo("filter=active");
+ }
+
+ @Test
+ void testCreateQueryParametersHandlesMixedOptionalAndRequired() throws URISyntaxException {
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("page", "1");
+ // filter is optional and not set
+
+ String result = RestProducer.createQueryParameters("page={page}&filter={filter?}", exchange);
+
+ assertThat(result).isEqualTo("page=1");
+ }
+
+ @Test
+ void testCreateQueryParametersFallsBackToVariable() throws URISyntaxException {
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.setVariable("orderId", "456");
+
+ String result = RestProducer.createQueryParameters("order={orderId}", exchange);
+
+ assertThat(result).isEqualTo("order=456");
+ }
+
+ @Test
+ void testCreateQueryParametersReturnsNullForNullInput() throws URISyntaxException {
+ Exchange exchange = new DefaultExchange(camelContext);
+
+ String result = RestProducer.createQueryParameters(null, exchange);
+
+ assertThat(result).isNull();
+ }
+
+ @Test
+ void testCreateQueryParametersHandlesUrlEncodedPlaceholder() throws URISyntaxException {
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("name", "John Doe");
+
+ String result = RestProducer.createQueryParameters("name=%7Bname%7D", exchange);
+
+ assertThat(result).isEqualTo("name=John+Doe");
+ }
+
+ @Test
+ void testCreateQueryParametersKeepsUnresolvedRequiredPlaceholder() throws URISyntaxException {
+ Exchange exchange = new DefaultExchange(camelContext);
+
+ String result = RestProducer.createQueryParameters("id={userId}", exchange);
+
+ assertThat(result).isEqualTo("id=%7BuserId%7D");
+ }
+
+ // ==================== PrepareExchange Tests ====================
+
+ @Test
+ void testPrepareExchangeSetsHttpMethodHeader() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:post:users?host=localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ producer.prepareExchange(exchange);
+
+ assertThat(exchange.getMessage().getHeader(RestConstants.HTTP_METHOD)).isEqualTo("POST");
+ }
+
+ @Test
+ void testPrepareExchangeSetsContentTypeFromProduces() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:post:users?host=localhost");
+ endpoint.setProduces("application/json");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ producer.prepareExchange(exchange);
+
+ assertThat(exchange.getMessage().getHeader(RestConstants.CONTENT_TYPE)).isEqualTo("application/json");
+ }
+
+ @Test
+ void testPrepareExchangeSetsAcceptFromConsumes() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ endpoint.setConsumes("application/xml");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ producer.prepareExchange(exchange);
+
+ assertThat(exchange.getMessage().getHeader(RestConstants.ACCEPT)).isEqualTo("application/xml");
+ }
+
+ @Test
+ void testPrepareExchangeDoesNotOverrideExistingContentType() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:post:users?host=localhost");
+ endpoint.setProduces("application/json");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader(RestConstants.CONTENT_TYPE, "text/plain");
+ producer.prepareExchange(exchange);
+
+ assertThat(exchange.getMessage().getHeader(RestConstants.CONTENT_TYPE)).isEqualTo("text/plain");
+ }
+
+ @Test
+ void testPrepareExchangeDoesNotOverrideExistingAccept() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ endpoint.setConsumes("application/xml");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader(RestConstants.ACCEPT, "text/html");
+ producer.prepareExchange(exchange);
+
+ assertThat(exchange.getMessage().getHeader(RestConstants.ACCEPT)).isEqualTo("text/html");
+ }
+
+ @Test
+ void testPrepareExchangeResolvesUriTemplateFromHeader() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users:{id}?host=http://localhost:8080");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("id", "123");
+ producer.prepareExchange(exchange);
+
+ String uri = exchange.getMessage().getHeader(RestConstants.REST_HTTP_URI, String.class);
+ assertThat(uri).isEqualTo("http://localhost:8080/users/123");
+ }
+
+ @Test
+ void testPrepareExchangeResolvesUriTemplateFromVariable() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:orders:{orderId}?host=http://localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.setVariable("orderId", "789");
+ producer.prepareExchange(exchange);
+
+ String uri = exchange.getMessage().getHeader(RestConstants.REST_HTTP_URI, String.class);
+ assertThat(uri).isEqualTo("http://localhost/orders/789");
+ }
+
+ @Test
+ void testPrepareExchangeSetsQueryParametersHeader() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ endpoint.setQueryParameters("page=1&size=10");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ producer.prepareExchange(exchange);
+
+ String query = exchange.getMessage().getHeader(RestConstants.REST_HTTP_QUERY, String.class);
+ assertThat(query).isEqualTo("page=1&size=10");
+ }
+
+ @Test
+ void testPrepareExchangeResolvesDynamicQueryParameters() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users?host=localhost");
+ endpoint.setQueryParameters("userId={id}");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("id", "456");
+ producer.prepareExchange(exchange);
+
+ String query = exchange.getMessage().getHeader(RestConstants.REST_HTTP_QUERY, String.class);
+ assertThat(query).isEqualTo("userId=456");
+ }
+
+ @Test
+ void testPrepareExchangeRemovesHttpPathWhenUriIsSet() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users:{id}?host=http://localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("id", "123");
+ exchange.getMessage().setHeader(Exchange.HTTP_PATH, "/old/path");
+ producer.prepareExchange(exchange);
+
+ assertThat(exchange.getMessage().getHeader(Exchange.HTTP_PATH)).isNull();
+ }
+
+ @Test
+ void testPrepareExchangeCombinesPathAndUriTemplate() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:api:users/{id}?host=http://localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("id", "999");
+ producer.prepareExchange(exchange);
+
+ String uri = exchange.getMessage().getHeader(RestConstants.REST_HTTP_URI, String.class);
+ assertThat(uri).isEqualTo("http://localhost/api/users/999");
+ }
+
+ @Test
+ void testPrepareExchangeDoesNotResolveTemplateWhenPrepareUriTemplateIsFalse() throws Exception {
+ RestEndpoint endpoint = (RestEndpoint) camelContext.getEndpoint("rest:get:users:{id}?host=http://localhost");
+ endpoint.setParameters(new HashMap<>());
+
+ RestConfiguration config = new RestConfiguration();
+ RestProducer producer = new RestProducer(endpoint, mockProducer, config);
+ producer.setPrepareUriTemplate(false);
+
+ Exchange exchange = new DefaultExchange(camelContext);
+ exchange.getMessage().setHeader("id", "123");
+ producer.prepareExchange(exchange);
+
+ String uri = exchange.getMessage().getHeader(RestConstants.REST_HTTP_URI, String.class);
+ assertThat(uri).isNull();
+ }
+}
diff --git a/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestRegistryStatefulTest.java b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestRegistryStatefulTest.java
new file mode 100644
index 0000000000000..4a7628ba52fbe
--- /dev/null
+++ b/components/camel-rest/src/test/java/org/apache/camel/component/rest/RestRegistryStatefulTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.rest;
+
+import java.util.List;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Processor;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.spi.RestRegistry;
+import org.apache.camel.support.DefaultConsumer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class RestRegistryStatefulTest {
+
+ private DefaultRestRegistry registry;
+ private CamelContext camelContext;
+
+ @Mock
+ private Endpoint mockEndpoint;
+
+ @Mock
+ private Processor mockProcessor;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ camelContext.start();
+
+ // Configure mock endpoint to return the CamelContext
+ when(mockEndpoint.getCamelContext()).thenReturn(camelContext);
+
+ registry = new DefaultRestRegistry();
+ registry.setCamelContext(camelContext);
+ registry.start();
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ registry.stop();
+ camelContext.stop();
+ }
+
+ @Test
+ void testServiceStateWithStatefulConsumer() throws Exception {
+ // Create a real DefaultConsumer which is a StatefulService
+ TestConsumer consumer = new TestConsumer(mockEndpoint, mockProcessor);
+
+ registry.addRestService(consumer, false, "http://localhost:8080/api/users",
+ "http://localhost:8080", "/api", "/users", "GET",
+ "application/json", "application/json", null, null,
+ "route1", "Get users");
+
+ List services = registry.listAllRestServices();
+ assertThat(services).hasSize(1);
+
+ // Before starting, status should be Stopped
+ assertThat(services.get(0).getState()).isEqualTo("Stopped");
+
+ // Start the consumer
+ consumer.start();
+
+ // After starting, status should be Started
+ assertThat(services.get(0).getState()).isEqualTo("Started");
+
+ // Stop the consumer
+ consumer.stop();
+
+ // After stopping, status should be Stopped
+ assertThat(services.get(0).getState()).isEqualTo("Stopped");
+ }
+
+ @Test
+ void testServiceStateWithStartedStatefulConsumer() throws Exception {
+ TestConsumer consumer = new TestConsumer(mockEndpoint, mockProcessor);
+ consumer.start();
+
+ registry.addRestService(consumer, false, "http://localhost:8080/api/orders",
+ "http://localhost:8080", "/api", "/orders", "POST",
+ null, null, null, null, "route2", "Create order");
+
+ List services = registry.listAllRestServices();
+ assertThat(services.get(0).getState()).isEqualTo("Started");
+
+ consumer.stop();
+ }
+
+ @Test
+ void testServiceStateWithSuspendedConsumer() throws Exception {
+ TestConsumer consumer = new TestConsumer(mockEndpoint, mockProcessor);
+ consumer.start();
+ consumer.suspend();
+
+ registry.addRestService(consumer, false, "http://localhost:8080/api/items",
+ "http://localhost:8080", "/api", "/items", "DELETE",
+ null, null, null, null, "route3", "Delete item");
+
+ List services = registry.listAllRestServices();
+ assertThat(services.get(0).getState()).isEqualTo("Suspended");
+
+ consumer.resume();
+ consumer.stop();
+ }
+
+ /**
+ * Test consumer that extends DefaultConsumer for testing stateful service behavior
+ */
+ private static class TestConsumer extends DefaultConsumer {
+
+ public TestConsumer(Endpoint endpoint, Processor processor) {
+ super(endpoint, processor);
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ // No-op for test
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ // No-op for test
+ }
+ }
+}