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

nfilotto pushed a commit to branch 276/integration-test-framework
in repository https://gitbox.apache.org/repos/asf/camel-karaf.git

commit 99965f44f646ad705344425b93599f3a7245ddad
Author: Francois de Parscau <francois.depars...@qlik.com>
AuthorDate: Mon Apr 29 22:11:45 2024 +0200

    Ref #276: Propose an integration test framework
---
 features/src/main/feature/camel-features.xml       |  16 ++-
 tests/camel-integration-test/pom.xml               |  23 +++-
 .../karaf/camel/itests/AbstractCamelComponent.java |   9 +-
 .../camel/itests/AbstractCamelKarafITest.java      |  13 +-
 .../karaf/camel/itests/ExternalResource.java       |  30 +++++
 .../camel/itests/GenericContainerResource.java     |  97 +++++++++++++++
 .../camel/itests/PaxExamWithExternalResource.java  | 138 +++++++++++++++++++++
 .../apache/karaf/camel/itests/TemporaryFile.java   |  74 +++++++++++
 .../camel/itests/UseExternalResourceProvider.java  |  26 ++++
 .../java/org/apache/karaf/camel/itests/Utils.java  |   6 +-
 tests/components/camel-elasticsearch/pom.xml       |  49 ++++++++
 .../camel/test/CamelElasticsearchComponent.java    |  79 ++++++++++++
 .../camel/itests/CamelElasticsearchITest.java      |  77 ++++++++++++
 .../karaf/camel/test/CamelJettyComponent.java      |   3 +-
 tests/components/pom.xml                           |   5 +-
 15 files changed, 623 insertions(+), 22 deletions(-)

diff --git a/features/src/main/feature/camel-features.xml 
b/features/src/main/feature/camel-features.xml
index 23548c9f..97c12f59 100644
--- a/features/src/main/feature/camel-features.xml
+++ b/features/src/main/feature/camel-features.xml
@@ -828,12 +828,18 @@
     <feature name='camel-elasticsearch' version='${project.version}' 
start-level='50'>
         <feature version='${camel.osgi.version.range}'>camel-core</feature>
         <feature prerequisite='true'>wrap</feature>
+        <feature prerequisite="true">spifly</feature>
         <feature version='[2.16,2.17)'>jackson</feature>
         <feature version="[4,5)">http-client</feature>
+        <bundle 
dependency="true">mvn:org.glassfish.hk2/osgi-resource-locator/2.5.0-b42</bundle>
+        <bundle 
dependency="true">mvn:org.eclipse.parsson/parsson/${parson-version}</bundle>
+        <bundle 
dependency='true'>mvn:jakarta.json/jakarta.json-api/${jakarta-json-api-version}</bundle>
+        <bundle 
dependency='true'>wrap:mvn:io.opentelemetry/opentelemetry-api/${opentelemetry-version}</bundle>
+        <bundle 
dependency='true'>wrap:mvn:io.opentelemetry/opentelemetry-context/${opentelemetry-version}</bundle>
         <bundle 
dependency='true'>wrap:mvn:org.apache.httpcomponents/httpasyncclient/${httpasyncclient-version}</bundle>
-        <bundle 
dependency='true'>wrap:mvn:co.elastic.clients/elasticsearch-java/8.12.1</bundle>
-        <bundle 
dependency='true'>wrap:mvn:org.elasticsearch.client/elasticsearch-rest-client/8.12.1</bundle>
-        <bundle 
dependency='true'>wrap:mvn:org.elasticsearch.client/elasticsearch-rest-client-sniffer/8.12.1</bundle>
+        <bundle 
dependency='true'>wrap:mvn:co.elastic.clients/elasticsearch-java/${elasticsearch-java-client-version}</bundle>
+        <bundle 
dependency='true'>wrap:mvn:org.elasticsearch.client/elasticsearch-rest-client/${elasticsearch-java-client-version}</bundle>
+        <bundle 
dependency='true'>wrap:mvn:org.elasticsearch.client/elasticsearch-rest-client-sniffer/${elasticsearch-java-client-sniffer-version}</bundle>
         
<bundle>mvn:org.apache.camel.karaf/camel-elasticsearch/${project.version}</bundle>
     </feature>
     <feature name='camel-elasticsearch-rest-client' 
version='${project.version}' start-level='50'>
@@ -842,8 +848,8 @@
         <feature version='[2.16,2.17)'>jackson</feature>
         <feature version="[4,5)">http-client</feature>
         <bundle 
dependency='true'>wrap:mvn:org.apache.httpcomponents/httpasyncclient/${httpasyncclient-version}</bundle>
-        <bundle 
dependency='true'>wrap:mvn:org.elasticsearch.client/elasticsearch-rest-client/8.12.1</bundle>
-        <bundle 
dependency='true'>wrap:mvn:org.elasticsearch.client/elasticsearch-rest-client-sniffer/8.12.1</bundle>
+        <bundle 
dependency='true'>wrap:mvn:org.elasticsearch.client/elasticsearch-rest-client/${elasticsearch-java-client-version}</bundle>
+        <bundle 
dependency='true'>wrap:mvn:org.elasticsearch.client/elasticsearch-rest-client-sniffer/${elasticsearch-java-client-sniffer-version}</bundle>
         
<bundle>mvn:org.apache.camel.karaf/camel-elasticsearch-rest-client/${project.version}</bundle>
     </feature>
     <feature name='camel-elytron' version='${project.version}' 
start-level='50'>
diff --git a/tests/camel-integration-test/pom.xml 
b/tests/camel-integration-test/pom.xml
index 36670d7d..caa119ee 100644
--- a/tests/camel-integration-test/pom.xml
+++ b/tests/camel-integration-test/pom.xml
@@ -71,15 +71,19 @@
                 <configuration>
                     <instructions>
                         <Export-Package>
-                            
org.apache.karaf.camel.itests;version=${camel.version}
+                            
org.apache.karaf.camel.itests;version=${project.version},
                         </Export-Package>
                         <Import-Package>
-                            
org.apache.karaf.itests,org.ops4j.pax.exam,org.osgi.framework,org.junit,
+                            org.apache.karaf.itests,
+                            org.ops4j.pax.exam,
+                            org.ops4j.pax.exam.options,
+                            org.osgi.framework,
+                            org.junit*,
                             
org.apache.camel*;${camel.osgi.import.camel.version},
                             org.apache.karaf.features,
+                            org.ops4j.pax.swissbox.tracker,
                             org.osgi.service.*,
-                            org.awaitility,
-                            org.ops4j.pax.swissbox.tracker
+                            org.awaitility*
                         </Import-Package>
                     </instructions>
                     
<excludeDependencies>geronimo-atinject_1.0_spec</excludeDependencies>
@@ -110,6 +114,11 @@
             <groupId>org.ops4j.pax.exam</groupId>
             <artifactId>pax-exam-container-karaf</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-junit4</artifactId>
+            <scope>compile</scope>
+        </dependency>
         <dependency>
             <groupId>org.awaitility</groupId>
             <artifactId>awaitility</artifactId>
@@ -167,5 +176,11 @@
             <version>${project.version}</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>testcontainers</artifactId>
+            <version>${testcontainers-version}</version>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 </project>
\ No newline at end of file
diff --git 
a/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/AbstractCamelComponent.java
 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/AbstractCamelComponent.java
index 21bb7f75..35d2a591 100644
--- 
a/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/AbstractCamelComponent.java
+++ 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/AbstractCamelComponent.java
@@ -60,6 +60,7 @@ public abstract class AbstractCamelComponent {
         return new RouteBuilder() {
             @Override
             public void configure() {
+                configureCamelContext(this.getCamelContext());
                 if (producerEnabled()) {
                     configureProducer(
                             this, 
from("timer:producer?repeatCount=1").routeId("producer-%s".formatted(getTestComponentName()))
@@ -72,6 +73,10 @@ public abstract class AbstractCamelComponent {
         };
     }
 
+    protected void configureCamelContext(CamelContext camelContext) {
+        //nothing by default
+    }
+
     protected boolean producerEnabled() {
         return true;
     }
@@ -94,8 +99,4 @@ public abstract class AbstractCamelComponent {
         }
         consumerRoute.routeId("consumer-%s".formatted(getTestComponentName()));
     }
-
-    public int getNextAvailablePort() {
-        return AbstractCamelKarafITest.getAvailablePort(30000, 40000);
-    }
 }
diff --git 
a/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/AbstractCamelKarafITest.java
 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/AbstractCamelKarafITest.java
index c210924e..66a757a5 100644
--- 
a/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/AbstractCamelKarafITest.java
+++ 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/AbstractCamelKarafITest.java
@@ -26,7 +26,6 @@ import org.osgi.framework.Bundle;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.ServerSocket;
-import java.util.stream.Stream;
 
 import org.apache.karaf.itests.KarafTestSupport;
 import org.junit.Assert;
@@ -39,6 +38,7 @@ import 
org.ops4j.pax.exam.karaf.options.KarafDistributionOption;
 import org.osgi.framework.BundleException;
 
 import static org.apache.karaf.camel.itests.Utils.toKebabCase;
+import static org.ops4j.pax.exam.OptionUtils.combine;
 
 public abstract class AbstractCamelKarafITest extends KarafTestSupport {
 
@@ -80,16 +80,19 @@ public abstract class AbstractCamelKarafITest extends 
KarafTestSupport {
         String sshPort = 
Integer.toString(getAvailablePort(Integer.parseInt(MIN_SSH_PORT), 
Integer.parseInt(MAX_SSH_PORT)));
 
         Option[] options = new Option[]{
-                
KarafDistributionOption.editConfigurationFilePut("etc/system.properties", 
"project.version", getVersion()),
-                
KarafDistributionOption.editConfigurationFileExtend("etc/system.properties", 
"project.target", getBaseDir()),
+                
CoreOptions.systemProperty("project.version").value(getVersion()),
+                
CoreOptions.systemProperty("project.target").value(getBaseDir()),
                 
KarafDistributionOption.features("mvn:org.apache.camel.karaf/apache-camel/"+ 
getVersion() + "/xml/features", "scr","camel-core"),
                 
CoreOptions.mavenBundle().groupId("org.apache.camel.karaf").artifactId("camel-integration-test").versionAsInProject(),
                 
KarafDistributionOption.editConfigurationFilePut("etc/org.ops4j.pax.web.cfg", 
"org.osgi.service.http.port", httpPort),
                 
KarafDistributionOption.editConfigurationFilePut("etc/org.apache.karaf.management.cfg",
 "rmiRegistryPort", rmiRegistryPort),
                 
KarafDistributionOption.editConfigurationFilePut("etc/org.apache.karaf.management.cfg",
 "rmiServerPort", rmiServerPort),
-                
KarafDistributionOption.editConfigurationFilePut("etc/org.apache.karaf.shell.cfg",
 "sshPort", sshPort),
+                
KarafDistributionOption.editConfigurationFilePut("etc/org.apache.karaf.shell.cfg",
 "sshPort", sshPort)
         };
-        return Stream.of(super.config(), 
options).flatMap(Stream::of).toArray(Option[]::new);
+        Option[] systemProperties = 
PaxExamWithExternalResource.systemProperties().entrySet().stream()
+                .map(e -> 
CoreOptions.systemProperty(e.getKey()).value(e.getValue()))
+                .toArray(Option[]::new);
+        return combine(combine(super.config(), options), systemProperties);
     }
 
     @Before
diff --git 
a/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/ExternalResource.java
 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/ExternalResource.java
new file mode 100644
index 00000000..858ee797
--- /dev/null
+++ 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/ExternalResource.java
@@ -0,0 +1,30 @@
+package org.apache.karaf.camel.itests;
+
+import java.util.Map;
+
+/**
+ * An interface representing an external resource to set up before a test and 
guarantee to tear it down afterward.
+ * Compared to an {@link org.junit.rules.ExternalResource}, the methods {@link 
#before()} and {@link #after()} are
+ * executed outside Karaf container, so it can be used to set up external 
resources like a database, a message broker,
+ * etc.
+ * @see TemporaryFile
+ * @see GenericContainerResource
+ */
+public interface ExternalResource {
+
+    /**
+     * Sets up the external resource.
+     */
+    void before();
+
+    /**
+     * Tears down the external resource.
+     */
+    void after();
+
+    /**
+     * Gives access to the properties of the external resource like a 
username, a password or a path, that will be
+     * provided to the Karaf instance as System properties.
+     */
+    Map<String, String> properties();
+}
diff --git 
a/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/GenericContainerResource.java
 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/GenericContainerResource.java
new file mode 100644
index 00000000..8a188cee
--- /dev/null
+++ 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/GenericContainerResource.java
@@ -0,0 +1,97 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.karaf.camel.itests;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.GenericContainer;
+
+/**
+ * A JUnit ExternalResource that starts and stops a TestContainer.
+ *
+ * @param <T> the type of the TestContainer
+ */
+public class GenericContainerResource<T extends GenericContainer<T>> 
implements ExternalResource {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(GenericContainerResource.class);
+    private final T container;
+    private final Map<String, String> properties = new HashMap<>();
+    private final List<ExternalResource> dependencies = new ArrayList<>();
+    private final Consumer<GenericContainerResource<T>> onStarted;
+
+    public GenericContainerResource(T container) {
+        this(container, t -> {});
+    }
+
+    /**
+     * Create a GenericContainerResource with the given TestContainer and a 
callback to be called when the container is
+     * started.
+     * @param container the TestContainer
+     * @param onStarted the callback to be called when the container is started
+     */
+    public GenericContainerResource(T container, 
Consumer<GenericContainerResource<T>> onStarted) {
+        this.container = container;
+        this.onStarted = onStarted;
+    }
+
+    @Override
+    public void before() {
+        container.start();
+        onStarted.accept(this);
+        for (ExternalResource dependency : dependencies) {
+            dependency.properties().forEach(this::setProperty);
+        }
+        LOG.info("Container {} started", container.getDockerImageName());
+    }
+
+    @Override
+    public void after() {
+        container.stop();
+        for (ExternalResource dependency : dependencies) {
+            try {
+                dependency.after();
+            } catch (Exception e) {
+                LOG.warn("Error cleaning dependency: {}", 
dependency.getClass().getName(), e);
+            }
+        }
+        LOG.info("Container {} stopped", container.getDockerImageName());
+    }
+
+    @Override
+    public Map<String, String> properties() {
+        return properties;
+    }
+
+    public T getContainer() {
+        return container;
+    }
+
+    public void setProperty(String key, String value) {
+        properties.put(key, value);
+    }
+
+    public void addDependency(ExternalResource dependency) {
+        dependencies.add(dependency);
+    }
+}
diff --git 
a/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/PaxExamWithExternalResource.java
 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/PaxExamWithExternalResource.java
new file mode 100644
index 00000000..4bd00056
--- /dev/null
+++ 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/PaxExamWithExternalResource.java
@@ -0,0 +1,138 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.karaf.camel.itests;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+import org.junit.runner.manipulation.Filter;
+import org.junit.runner.manipulation.Filterable;
+import org.junit.runner.manipulation.NoTestsRemainException;
+import org.junit.runner.manipulation.Sortable;
+import org.junit.runner.manipulation.Sorter;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.ParentRunner;
+import org.junit.runners.model.InitializationError;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.junit.impl.ProbeRunner;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A fork of {@link PaxExam} that supports external resources which can be 
created and destroyed outside Karaf.
+ * <p>
+ * This runner is intended to be used with {@link UseExternalResourceProvider} 
annotation in order to create the
+ * external resources that are needed by the test. Please note that due to the 
way PaxExam works, the class cannot be
+ * the same as the test class, but it can be a static inner class of it 
otherwise the class will need to be resolved
+ * within Karaf which is what we want to avoid.
+ *
+ * @see UseExternalResourceProvider
+ */
+public class PaxExamWithExternalResource extends Runner implements Filterable, 
Sortable {
+    private static final Logger LOG = 
LoggerFactory.getLogger(PaxExamWithExternalResource.class);
+    private static final ThreadLocal<PaxExamWithExternalResource> current = 
new ThreadLocal<>();
+    private final ParentRunner<?> delegate;
+    private final List<ExternalResource> externalResources;
+
+    public PaxExamWithExternalResource(Class<?> testClass) throws 
InitializationError, InvocationTargetException,
+            IllegalAccessException {
+        this.externalResources = beforeAll(testClass);
+        try {
+            current.set(this);
+            this.delegate = new ProbeRunner(testClass);
+        } finally {
+            current.remove();
+        }
+    }
+
+    private List<ExternalResource> beforeAll(Class<?> testClass) throws 
InvocationTargetException, IllegalAccessException {
+        UseExternalResourceProvider annotation = 
testClass.getAnnotation(UseExternalResourceProvider.class);
+        if (annotation != null) {
+            List<ExternalResource> result = new ArrayList<>();
+            for (Method m : annotation.value().getMethods()) {
+                if (isExternalResourceSupplier(m)) {
+                    ExternalResource externalResource = (ExternalResource) 
m.invoke(null);
+                    externalResource.before();
+                    result.add(externalResource);
+                }
+            }
+            return result;
+        }
+        LOG.warn("Class {} is not annotated with 
@UseExternalResourceProvider", testClass.getName());
+        return List.of();
+    }
+
+    private boolean isExternalResourceSupplier(Method m) {
+        return ExternalResource.class.isAssignableFrom(m.getReturnType()) && 
m.getParameterTypes().length == 0
+                && Modifier.isStatic(m.getModifiers());
+    }
+
+    @Override
+    public Description getDescription() {
+        return delegate.getDescription();
+    }
+
+    @Override
+    public void run(RunNotifier notifier) {
+        try {
+            delegate.run(notifier);
+        } finally {
+            afterAll();
+        }
+    }
+
+    private void afterAll() {
+        for (int i = externalResources.size() - 1; i >= 0; i--) {
+            try {
+                externalResources.get(i).after();
+            } catch (Exception e) {
+                LOG.warn("Error while cleaning up external resource", e);
+            }
+        }
+    }
+
+    @Override
+    public void filter(Filter filter) throws NoTestsRemainException {
+        delegate.filter(filter);
+    }
+
+    @Override
+    public void sort(Sorter sorter) {
+        delegate.sort(sorter);
+    }
+
+    static Map<String, String> systemProperties() {
+        PaxExamWithExternalResource value = current.get();
+        if (value == null) {
+            return Map.of();
+        }
+        return value.externalResources.stream()
+                .map(ExternalResource::properties)
+                .map(Map::entrySet)
+                .flatMap(Set::stream)
+                .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, 
Map.Entry::getValue));
+    }
+}
diff --git 
a/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/TemporaryFile.java
 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/TemporaryFile.java
new file mode 100644
index 00000000..9e609402
--- /dev/null
+++ 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/TemporaryFile.java
@@ -0,0 +1,74 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.karaf.camel.itests;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A temporary file that is deleted when the test is finished.
+ */
+public class TemporaryFile implements ExternalResource {
+    private static final Logger LOG = 
LoggerFactory.getLogger(TemporaryFile.class);
+    private final String key;
+    private final Path path;
+
+    /**
+     * Create a temporary file with the given key.
+     *
+     * @param key the key to store the path of the temporary file
+     * @param prefix the prefix of the temporary file
+     * @param suffix the suffix of the temporary file
+     * @throws IOException if an I/O error occurs
+     */
+    public TemporaryFile(String key, String prefix, String suffix) throws 
IOException {
+        this.key = key;
+        this.path = Files.createTempFile(prefix, suffix);
+    }
+
+    @Override
+    public void before() {
+        // Do nothing
+    }
+
+    @Override
+    public void after() {
+        try {
+            if (Files.deleteIfExists(path)) {
+                LOG.debug("Deleted temporary file: {}", path);
+            }
+        } catch (IOException e) {
+            LOG.warn("Failed to delete temporary file: {}", path, e);
+        }
+    }
+
+    @Override
+    public Map<String, String> properties() {
+        return Map.of(key, path.toString());
+    }
+
+    public Path getPath() {
+        return path;
+    }
+}
diff --git 
a/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/UseExternalResourceProvider.java
 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/UseExternalResourceProvider.java
new file mode 100644
index 00000000..b9795bb0
--- /dev/null
+++ 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/UseExternalResourceProvider.java
@@ -0,0 +1,26 @@
+package org.apache.karaf.camel.itests;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to specify the class that provides the methods to create all the 
external resources required by the test.
+ * In the provider class, each public static method that returns an instance 
of a subtype of {@link ExternalResource}
+ * with no parameters is considered as an {@link ExternalResource} supplier, 
so it will be invoked before executing
+ * the test and {@code PaxExamWithExternalResource} will take care of its 
lifecycle making sure that it is created and
+ * destroyed outside Karaf.
+ *
+ * @see PaxExamWithExternalResource
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Inherited
+public @interface UseExternalResourceProvider {
+    /**
+     * The external resource provider class.
+     */
+    Class<?> value();
+}
diff --git 
a/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/Utils.java
 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/Utils.java
index 67390589..d94cb448 100644
--- 
a/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/Utils.java
+++ 
b/tests/camel-integration-test/src/main/java/org/apache/karaf/camel/itests/Utils.java
@@ -16,7 +16,7 @@
  */
 package org.apache.karaf.camel.itests;
 
-final class Utils {
+public final class Utils {
 
     private Utils() {
     }
@@ -24,4 +24,8 @@ final class Utils {
     static String toKebabCase(String name) {
         return name.replaceAll("([a-z0-9])([A-Z])", "$1-$2").toLowerCase();
     }
+
+    public static int getNextAvailablePort() {
+        return AbstractCamelKarafITest.getAvailablePort(30000, 40000);
+    }
 }
diff --git a/tests/components/camel-elasticsearch/pom.xml 
b/tests/components/camel-elasticsearch/pom.xml
new file mode 100644
index 00000000..10fcaf35
--- /dev/null
+++ b/tests/components/camel-elasticsearch/pom.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.camel.karaf</groupId>
+        <artifactId>camel-karaf-components-test</artifactId>
+        <version>4.5.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>camel-elasticsearch-test</artifactId>
+    <name>Apache Camel :: Karaf :: Tests :: Components :: ElasticSearch Java 
API Client</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-elasticsearch</artifactId>
+            <version>${camel.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>elasticsearch</artifactId>
+            <version>${testcontainers-version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+
+</project>
\ No newline at end of file
diff --git 
a/tests/components/camel-elasticsearch/src/main/java/org/apache/karaf/camel/test/CamelElasticsearchComponent.java
 
b/tests/components/camel-elasticsearch/src/main/java/org/apache/karaf/camel/test/CamelElasticsearchComponent.java
new file mode 100644
index 00000000..6780894b
--- /dev/null
+++ 
b/tests/components/camel-elasticsearch/src/main/java/org/apache/karaf/camel/test/CamelElasticsearchComponent.java
@@ -0,0 +1,79 @@
+/*
+ * 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.karaf.camel.test;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.es.ElasticsearchComponent;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.karaf.camel.itests.AbstractCamelComponentResultMockBased;
+import org.osgi.service.component.annotations.Component;
+
+@Component(name = "karaf-camel-elasticsearch-test", immediate = true)
+public class CamelElasticsearchComponent extends 
AbstractCamelComponentResultMockBased {
+
+    private static final String INDEX_NAME = "testindex";
+
+    @Override
+    protected void configureCamelContext(CamelContext camelContext) {
+        final ElasticsearchComponent elasticsearchComponent = new 
ElasticsearchComponent();
+        elasticsearchComponent.setEnableSSL(true);
+        elasticsearchComponent.setHostAddresses(
+            "%s:%s".formatted(System.getProperty("elasticsearch.host"), 
System.getProperty("elasticsearch.port"))
+        );
+        
elasticsearchComponent.setUser(System.getProperty("elasticsearch.username"));
+        
elasticsearchComponent.setPassword(System.getProperty("elasticsearch.password"));
+        
elasticsearchComponent.setCertificatePath("file:%s".formatted(System.getProperty("elasticsearch.cafile")));
+
+        camelContext.addComponent("elasticsearch",elasticsearchComponent);
+    }
+
+    @Override
+    protected void configureProducer(RouteBuilder builder, RouteDefinition 
producerRoute) {
+        //to add the mock endpoint at the end of the route, call 
configureConsumer
+        configureConsumer(
+            
producerRoute.toF("elasticsearch://elasticsearch?operation=Exists&indexName=%s",
 INDEX_NAME)
+                    .log("Index exist: ${body}")
+                    .setBody(builder.simple("""
+                            {"date": "${header.CamelTimerFiredTime}", 
"someKey": "someValue"}
+                            """))
+                    
.toF("elasticsearch://elasticsearch?operation=Index&indexName=%s", INDEX_NAME)
+                    .log("Index doc : ${body}")
+                    .setHeader("_ID", builder.simple("${body}"))
+                    
.toF("elasticsearch://elasticsearch?operation=GetById&indexName=%s", INDEX_NAME)
+                    .log("Get doc: ${body}")
+                    .setHeader("indexId", builder.simple("${header._ID}"))
+                    .setBody(builder.constant("""
+                            {"doc": {"someKey": "someValue2"}}
+                            """))
+                    
.toF("elasticsearch://elasticsearch?operation=Update&indexName=%s", INDEX_NAME)
+                    .log("Update doc: ${body} ")
+                    .setBody(builder.simple("${header._ID}"))
+                    
.toF("elasticsearch://elasticsearch?operation=GetById&indexName=%s", INDEX_NAME)
+                    .log("Get doc: ${body}")
+                    .setBody(builder.simple("${header._ID}"))
+                    
.toF("elasticsearch://elasticsearch?operation=Delete&indexName=%s", INDEX_NAME)
+                    .log("Delete doc: ${body}")
+                    .setBody(builder.constant("OK"))
+        );
+
+    }
+
+    @Override
+    protected boolean consumerEnabled() {
+        return false;
+    }
+}
\ No newline at end of file
diff --git 
a/tests/components/camel-elasticsearch/src/test/java/org/apache/karaf/camel/itests/CamelElasticsearchITest.java
 
b/tests/components/camel-elasticsearch/src/test/java/org/apache/karaf/camel/itests/CamelElasticsearchITest.java
new file mode 100644
index 00000000..dc349c7a
--- /dev/null
+++ 
b/tests/components/camel-elasticsearch/src/test/java/org/apache/karaf/camel/itests/CamelElasticsearchITest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed 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.karaf.camel.itests;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.time.Duration;
+
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
+import org.testcontainers.elasticsearch.ElasticsearchContainer;
+
+@UseExternalResourceProvider(CamelElasticsearchITest.ExternalResourceProviders.class)
+@RunWith(PaxExamWithExternalResource.class)
+@ExamReactorStrategy(PerClass.class)
+public class CamelElasticsearchITest extends 
AbstractCamelKarafResultMockBasedITest {
+
+    @Override
+    protected void configureMock(MockEndpoint mock) {
+        mock.expectedBodiesReceived("OK");
+    }
+
+    @Test
+    public void testResultMock() throws Exception {
+        assertMockEndpointsSatisfied();
+    }
+
+    public static final class ExternalResourceProviders {
+
+        private static final String USER_NAME = "elastic";
+        private static final String PASSWORD = "s3cret";
+        private static final int ELASTIC_SEARCH_PORT = 9200;
+
+        public static GenericContainerResource<ElasticsearchContainer> 
createElasticsearchContainer() {
+            final ElasticsearchContainer elasticsearchContainer =
+                    new 
ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:8.11.1").withPassword(PASSWORD);
+            // Increase the timeout from 60 seconds to 90 seconds to ensure 
that it will be long enough
+            // on the build pipeline
+            elasticsearchContainer.setWaitStrategy(
+                    new LogMessageWaitStrategy()
+                            
.withRegEx(".*(\"message\":\\s?\"started[\\s?|\"].*|] started\n$)")
+                            .withStartupTimeout(Duration.ofSeconds(90)));
+            return new GenericContainerResource<>(elasticsearchContainer,
+                    resource -> {
+                        
resource.getContainer().caCertAsBytes().ifPresent(content -> {
+                            try {
+                                TemporaryFile tempFile = new 
TemporaryFile("elasticsearch.cafile", "http_ca", ".crt");
+                                Files.write(tempFile.getPath(), content);
+                                resource.addDependency(tempFile);
+                            } catch (IOException e) {
+                                throw new RuntimeException(e);
+                            }
+                        });
+                        resource.setProperty("elasticsearch.host", 
elasticsearchContainer.getHost());
+                        resource.setProperty("elasticsearch.port", 
Integer.toString(elasticsearchContainer.getMappedPort(ELASTIC_SEARCH_PORT)));
+                        resource.setProperty("elasticsearch.username", 
USER_NAME);
+                        resource.setProperty("elasticsearch.password", 
PASSWORD);
+                    }
+            );
+        }
+    }
+}
\ No newline at end of file
diff --git 
a/tests/components/camel-jetty/src/main/java/org/apache/karaf/camel/test/CamelJettyComponent.java
 
b/tests/components/camel-jetty/src/main/java/org/apache/karaf/camel/test/CamelJettyComponent.java
index 0bd93e9a..a9c2ff0a 100644
--- 
a/tests/components/camel-jetty/src/main/java/org/apache/karaf/camel/test/CamelJettyComponent.java
+++ 
b/tests/components/camel-jetty/src/main/java/org/apache/karaf/camel/test/CamelJettyComponent.java
@@ -26,6 +26,7 @@ import org.apache.camel.Processor;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.model.RouteDefinition;
 import org.apache.karaf.camel.itests.AbstractCamelComponentResultMockBased;
+import org.apache.karaf.camel.itests.Utils;
 import org.osgi.service.component.annotations.Component;
 
 @Component(
@@ -34,7 +35,7 @@ import org.osgi.service.component.annotations.Component;
 )
 public class CamelJettyComponent extends AbstractCamelComponentResultMockBased 
{
 
-    private final int port = getNextAvailablePort();
+    private final int port = Utils.getNextAvailablePort();
 
     @Override
     protected Function<RouteBuilder, RouteDefinition> consumerRoute() {
diff --git a/tests/components/pom.xml b/tests/components/pom.xml
index e26df75d..c3fdae23 100644
--- a/tests/components/pom.xml
+++ b/tests/components/pom.xml
@@ -39,6 +39,7 @@
     <modules>
         <module>camel-file</module>
         <module>camel-seda</module>
+        <module>camel-elasticsearch</module>
         <module>camel-jetty</module>
     </modules>
 
@@ -182,7 +183,7 @@
                 <configuration>
                     <instructions>
                         <Export-Package>
-                            
org.apache.karaf.camel.test;version=${camel.version}
+                            
org.apache.karaf.camel.test;version=${project.version}
                         </Export-Package>
                         <Import-Package>
                             
org.apache.camel*;${camel.osgi.import.camel.version},
@@ -233,7 +234,7 @@
                                 
<project.version>${project.version}</project.version>
                                 
<project.target>${project.build.directory}</project.target>
                                 
<integration.test.project.resources>${integration.test.project.resources}</integration.test.project.resources>
-                                
<org.ops4j.pax.logging.DefaultServiceLog.level>ERROR</org.ops4j.pax.logging.DefaultServiceLog.level>
+                                
<org.ops4j.pax.logging.DefaultServiceLog.level>WARN</org.ops4j.pax.logging.DefaultServiceLog.level>
                             </systemPropertyVariables>
                             
<forkedProcessExitTimeoutInSeconds>5</forkedProcessExitTimeoutInSeconds>
                         </configuration>


Reply via email to