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

Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-spring-boot-examples.git


The following commit(s) were added to refs/heads/main by this push:
     new 5f9f5719 CSB-8935: Add blob index tags routes and test (#218)
5f9f5719 is described below

commit 5f9f5719fa94ed57159cb69a8517b0ee9948fce5
Author: Thomas Diesler <[email protected]>
AuthorDate: Tue Oct 6 10:47:17 2026 +0200

    CSB-8935: Add blob index tags routes and test (#218)
    
    * CSB-8935: Add blob index tags routes and test
    
    - Introduces the camel-example-spring-boot-azure-storage-blob module with 
basic blob upload/download
    - Adds setBlobTags, getBlobTags and findBlobsByTags routes
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CSB-8938: Add blob snapshot routes and test
    
    - Adds createSnapshot route for snapshot creation/reading blob content
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 README.adoc                                        |   4 +-
 .../README.adoc                                    |  37 +++++
 .../pom.xml                                        | 102 ++++++++++++++
 .../main/java/camel/sample/BlobConfiguration.java  |  44 ++++++
 .../main/java/camel/sample/BlobRouteBuilder.java   |  38 +++++
 .../camel/sample/BlobSnapshotRouteBuilder.java     |  32 +++++
 .../java/camel/sample/BlobStorageApplication.java  |  28 ++++
 .../java/camel/sample/BlobTagsRouteBuilder.java    |  42 ++++++
 .../src/main/resources/application.yml             |  29 ++++
 .../src/test/java/camel/sample/BlobSmokeTest.java  | 134 ++++++++++++++++++
 .../test/java/camel/sample/BlobSnapshotTest.java   | 156 +++++++++++++++++++++
 .../src/test/java/camel/sample/BlobTagsTest.java   | 139 ++++++++++++++++++
 azure/pom.xml                                      |   1 +
 13 files changed, 785 insertions(+), 1 deletion(-)

diff --git a/README.adoc b/README.adoc
index b59988ad..9aada038 100644
--- a/README.adoc
+++ b/README.adoc
@@ -27,7 +27,7 @@ readme's instructions.
 === Examples
 
 // examples: START
-Number of Examples: 72 (0 deprecated)
+Number of Examples: 73 (0 deprecated)
 
 [width="100%",cols="4,2,4",options="header"]
 |===
@@ -81,6 +81,8 @@ Number of Examples: 72 (0 deprecated)
 
 | link:azure/camel-example-spring-boot-azure-servicebus/README.adoc[Spring 
Boot Azure Servicebus] (camel-example-spring-boot-azure-servicebus) | Cloud | 
An example showing how to work with Camel, Azure Service Bus and Spring Boot
 
+| link:azure/camel-example-spring-boot-azure-storage-blob/README.adoc[Spring 
Boot Azure Storage Blob] (camel-example-spring-boot-azure-storage-blob) | Cloud 
| An example showing how to work with Camel, Azure Storage Blob and Spring Boot
+
 | link:infinispan/README.adoc[Infinispan] (infinispan) | Cloud | An example 
showing the Camel Infinispan component with Spring Boot
 
 | link:webhook/readme.adoc[Webhook] (webhook) | Cloud | Example on how to use 
the Camel Webhook component
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/README.adoc 
b/azure/camel-example-spring-boot-azure-storage-blob/README.adoc
new file mode 100644
index 00000000..02a401ba
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/README.adoc
@@ -0,0 +1,37 @@
+== Camel Azure Storage Blob Example
+
+=== Introduction
+
+This example demonstrates how to use the Apache Camel Azure Storage Blob 
component with Spring Boot to:
+
+* Upload block blobs
+* Set and get blob index tags
+* Find blobs by tag filter expressions
+* Create and read blob snapshots
+
+=== Running Tests (Azurite)
+
+Tests run automatically against https://github.com/Azure/Azurite[Azurite] 
(Azure Storage emulator) via Testcontainers. Docker must be running.
+
+    mvn verify
+
+=== Running with Azure
+
+To run against a real Azure Storage account:
+
+1. Create a General-purpose v2 Storage Account in the Azure portal
+2. Enable blob versioning under Data management > Data protection (optional, 
for version operations)
+3. Get your account name and access key from Security + networking > Access 
keys
+
+    export AZURE_STORAGE_ACCOUNT_NAME=youraccount
+    export AZURE_STORAGE_ACCESS_KEY=yourkey
+    export AZURE_STORAGE_CONTAINER_NAME=yourcontainer
+    mvn spring-boot:run
+
+=== Help and Contributions
+
+If you hit any problems please let us know on the 
https://camel.apache.org/community/mailing-list/[Camel Forums].
+
+Please help us make Apache Camel better - we appreciate any feedback you may 
have.
+
+https://camel.apache.org/community/contributing/[Contributing]
diff --git a/azure/camel-example-spring-boot-azure-storage-blob/pom.xml 
b/azure/camel-example-spring-boot-azure-storage-blob/pom.xml
new file mode 100644
index 00000000..915f9f56
--- /dev/null
+++ b/azure/camel-example-spring-boot-azure-storage-blob/pom.xml
@@ -0,0 +1,102 @@
+<?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.springboot.example</groupId>
+    <artifactId>camel-example-spring-boot-azure</artifactId>
+    <version>4.23.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>camel-example-spring-boot-azure-storage-blob</artifactId>
+  <name>Camel SB Examples :: Azure Storage Blob</name>
+  <description>An example showing how to work with Camel, Azure Storage Blob 
and Spring Boot</description>
+
+  <properties>
+    <category>Cloud</category>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter</artifactId>
+    </dependency>
+
+    <!-- Camel -->
+    <dependency>
+      <groupId>org.apache.camel.springboot</groupId>
+      <artifactId>camel-spring-boot-starter</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.camel.springboot</groupId>
+      <artifactId>camel-azure-storage-blob-starter</artifactId>
+    </dependency>
+
+    <!-- Test -->
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.camel</groupId>
+      <artifactId>camel-test-spring-junit6</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-testcontainers</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>testcontainers</artifactId>
+      <version>${testcontainers-version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.testcontainers</groupId>
+      <artifactId>testcontainers-junit-jupiter</artifactId>
+      <version>${testcontainers-version}</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-maven-plugin</artifactId>
+        <version>${spring-boot-version}</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>repackage</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git 
a/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobConfiguration.java
 
b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobConfiguration.java
new file mode 100644
index 00000000..db96c378
--- /dev/null
+++ 
b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobConfiguration.java
@@ -0,0 +1,44 @@
+/*
+ * 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 camel.sample;
+
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.azure.storage.common.StorageSharedKeyCredential;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class BlobConfiguration {
+
+    @Value("${azure.storage.account-name}")
+    private String accountName;
+
+    @Value("${azure.storage.access-key}")
+    private String accessKey;
+
+    @Bean
+    BlobServiceClient serviceClient() {
+        String endpoint = String.format("https://%s.blob.core.windows.net";, 
accountName);
+        return new BlobServiceClientBuilder()
+                .endpoint(endpoint)
+                .credential(new StorageSharedKeyCredential(accountName, 
accessKey))
+                .buildClient();
+    }
+}
diff --git 
a/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobRouteBuilder.java
 
b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobRouteBuilder.java
new file mode 100644
index 00000000..34c2e638
--- /dev/null
+++ 
b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobRouteBuilder.java
@@ -0,0 +1,38 @@
+/*
+ * 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 camel.sample;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.springframework.stereotype.Component;
+
+@Component
+public class BlobRouteBuilder extends RouteBuilder {
+
+    @Override
+    public void configure() {
+        from("direct:uploadBlob")
+            
.to("azure-storage-blob://{{azure.storage.account-name}}/{{azure.storage.container-name}}"
+                + "?operation=uploadBlockBlob"
+                + "&serviceClient=#serviceClient");
+
+        from("direct:getBlob")
+            
.to("azure-storage-blob://{{azure.storage.account-name}}/{{azure.storage.container-name}}"
+                + "?operation=getBlob"
+                + "&serviceClient=#serviceClient")
+            .convertBodyTo(byte[].class);
+    }
+}
diff --git 
a/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobSnapshotRouteBuilder.java
 
b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobSnapshotRouteBuilder.java
new file mode 100644
index 00000000..7dd4b7d2
--- /dev/null
+++ 
b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobSnapshotRouteBuilder.java
@@ -0,0 +1,32 @@
+/*
+ * 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 camel.sample;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.springframework.stereotype.Component;
+
+@Component
+public class BlobSnapshotRouteBuilder extends RouteBuilder {
+
+    @Override
+    public void configure() {
+        from("direct:createSnapshot")
+            
.to("azure-storage-blob://{{azure.storage.account-name}}/{{azure.storage.container-name}}"
+                + "?operation=createBlobSnapshot"
+                + "&serviceClient=#serviceClient");
+    }
+}
diff --git 
a/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobStorageApplication.java
 
b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobStorageApplication.java
new file mode 100644
index 00000000..b23df7aa
--- /dev/null
+++ 
b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobStorageApplication.java
@@ -0,0 +1,28 @@
+/*
+ * 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 camel.sample;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class BlobStorageApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(BlobStorageApplication.class, args);
+    }
+}
diff --git 
a/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobTagsRouteBuilder.java
 
b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobTagsRouteBuilder.java
new file mode 100644
index 00000000..a55fc40e
--- /dev/null
+++ 
b/azure/camel-example-spring-boot-azure-storage-blob/src/main/java/camel/sample/BlobTagsRouteBuilder.java
@@ -0,0 +1,42 @@
+/*
+ * 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 camel.sample;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.springframework.stereotype.Component;
+
+@Component
+public class BlobTagsRouteBuilder extends RouteBuilder {
+
+    @Override
+    public void configure() {
+        from("direct:setBlobTags")
+            
.to("azure-storage-blob://{{azure.storage.account-name}}/{{azure.storage.container-name}}"
+                + "?operation=setBlobTags"
+                + "&serviceClient=#serviceClient");
+
+        from("direct:getBlobTags")
+            
.to("azure-storage-blob://{{azure.storage.account-name}}/{{azure.storage.container-name}}"
+                + "?operation=getBlobTags"
+                + "&serviceClient=#serviceClient");
+
+        from("direct:findBlobsByTags")
+            
.to("azure-storage-blob://{{azure.storage.account-name}}/{{azure.storage.container-name}}"
+                + "?operation=findBlobsByTags"
+                + "&serviceClient=#serviceClient");
+    }
+}
diff --git 
a/azure/camel-example-spring-boot-azure-storage-blob/src/main/resources/application.yml
 
b/azure/camel-example-spring-boot-azure-storage-blob/src/main/resources/application.yml
new file mode 100644
index 00000000..27d640bd
--- /dev/null
+++ 
b/azure/camel-example-spring-boot-azure-storage-blob/src/main/resources/application.yml
@@ -0,0 +1,29 @@
+#
+# 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.
+#
+
+azure:
+  storage:
+    account-name: ${AZURE_STORAGE_ACCOUNT_NAME:devstoreaccount1}
+    access-key: ${AZURE_STORAGE_ACCESS_KEY}
+    container-name: ${AZURE_STORAGE_CONTAINER_NAME:testcontainer}
+
+camel:
+  component:
+    azure-storage-blob:
+      credential-type: SHARED_KEY_CREDENTIAL
+  springboot:
+    main-run-controller: true
diff --git 
a/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobSmokeTest.java
 
b/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobSmokeTest.java
new file mode 100644
index 00000000..dd835c8e
--- /dev/null
+++ 
b/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobSmokeTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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 camel.sample;
+
+import java.io.ByteArrayInputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.azure.storage.common.StorageSharedKeyCredential;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.component.azure.storage.blob.BlobConstants;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DirtiesContext
+@CamelSpringBootTest
+@SpringBootTest(classes = {
+        CamelAutoConfiguration.class,
+        BlobSmokeTest.class,
+        BlobSmokeTest.TestConfiguration.class,
+        BlobRouteBuilder.class
+})
+public class BlobSmokeTest {
+
+    static final String ACCOUNT_NAME = "devstoreaccount1";
+    static final String ACCOUNT_KEY =
+            
"Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
+    static final String CONTAINER_NAME = "smoketest";
+
+    static GenericContainer<?> azurite = new 
GenericContainer<>("mcr.microsoft.com/azure-storage/azurite:3.35.0")
+            .withExposedPorts(10000)
+            .waitingFor(Wait.forListeningPort());
+
+    static {
+        azurite.start();
+    }
+
+    @DynamicPropertySource
+    static void azuriteProperties(DynamicPropertyRegistry registry) {
+        registry.add("azure.storage.account-name", () -> ACCOUNT_NAME);
+        registry.add("azure.storage.container-name", () -> CONTAINER_NAME);
+        registry.add("camel.component.azure-storage-blob.credential-type", () 
-> "SHARED_KEY_CREDENTIAL");
+    }
+
+    @Autowired
+    CamelContext context;
+
+    @Autowired
+    ProducerTemplate template;
+
+    @Test
+    void testAzuriteReachable() throws Exception {
+        String url = String.format("http://%s:%d/devstoreaccount1?comp=list";,
+                azurite.getHost(), azurite.getMappedPort(10000));
+        HttpURLConnection conn = (HttpURLConnection) 
URI.create(url).toURL().openConnection();
+        conn.setRequestMethod("GET");
+        int status = conn.getResponseCode();
+        assertTrue(status == 200 || status == 403, "Azurite should be 
reachable, got " + status);
+    }
+
+    @Test
+    void testServiceClientInRegistry() {
+        BlobServiceClient client = 
context.getRegistry().lookupByNameAndType("serviceClient", 
BlobServiceClient.class);
+        assertNotNull(client, "serviceClient should be in the Camel registry");
+    }
+
+    @Test
+    void testUploadAndDownloadBlob() {
+        BlobServiceClient client = 
context.getRegistry().lookupByNameAndType("serviceClient", 
BlobServiceClient.class);
+        client.createBlobContainerIfNotExists(CONTAINER_NAME);
+
+        String expected = "hello azurite";
+        template.send("direct:uploadBlob", exchange -> {
+            exchange.getIn().setHeader(BlobConstants.BLOB_NAME, "smoke.txt");
+            exchange.getIn().setBody(new 
ByteArrayInputStream(expected.getBytes(StandardCharsets.UTF_8)));
+        });
+
+        Exchange result = template.send("direct:getBlob", exchange -> {
+            exchange.getIn().setHeader(BlobConstants.BLOB_NAME, "smoke.txt");
+        });
+        assertNull(result.getException(), "Download should succeed: " + 
result.getException());
+        assertEquals(expected, result.getMessage().getBody(String.class));
+    }
+
+    @Configuration
+    public static class TestConfiguration {
+        @Bean
+        BlobServiceClient serviceClient() {
+            String endpoint = String.format("http://%s:%d/%s";,
+                    azurite.getHost(), azurite.getMappedPort(10000), 
ACCOUNT_NAME);
+            return new BlobServiceClientBuilder()
+                    .endpoint(endpoint)
+                    .credential(new StorageSharedKeyCredential(ACCOUNT_NAME, 
ACCOUNT_KEY))
+                    .buildClient();
+        }
+    }
+}
diff --git 
a/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobSnapshotTest.java
 
b/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobSnapshotTest.java
new file mode 100644
index 00000000..dc6efdf9
--- /dev/null
+++ 
b/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobSnapshotTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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 camel.sample;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.azure.storage.common.StorageSharedKeyCredential;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.component.azure.storage.blob.BlobConstants;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@DirtiesContext
+@CamelSpringBootTest
+@SpringBootTest(classes = {
+        CamelAutoConfiguration.class,
+        BlobSnapshotTest.class,
+        BlobSnapshotTest.AzuriteConfiguration.class,
+        BlobRouteBuilder.class,
+        BlobSnapshotRouteBuilder.class
+})
+public class BlobSnapshotTest {
+
+    static final String ACCOUNT_NAME = "devstoreaccount1";
+    static final String ACCOUNT_KEY =
+            
"Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
+    static final String CONTAINER_NAME = "snapshottest";
+    static final String BLOB_NAME = "snapshot-blob.txt";
+    static final String ORIGINAL_CONTENT = "original snapshot content";
+
+    static GenericContainer<?> azurite = new 
GenericContainer<>("mcr.microsoft.com/azure-storage/azurite:3.35.0")
+            .withExposedPorts(10000)
+            .waitingFor(Wait.forListeningPort());
+
+    static {
+        azurite.start();
+    }
+
+    @DynamicPropertySource
+    static void azuriteProperties(DynamicPropertyRegistry registry) {
+        registry.add("azure.storage.account-name", () -> ACCOUNT_NAME);
+        registry.add("azure.storage.container-name", () -> CONTAINER_NAME);
+        registry.add("camel.component.azure-storage-blob.credential-type", () 
-> "SHARED_KEY_CREDENTIAL");
+    }
+
+    @Configuration
+    static class AzuriteConfiguration {
+        @Bean
+        BlobServiceClient serviceClient() {
+            String endpoint = String.format("http://%s:%d/%s";,
+                    azurite.getHost(), azurite.getMappedPort(10000), 
ACCOUNT_NAME);
+            return new BlobServiceClientBuilder()
+                    .endpoint(endpoint)
+                    .credential(new StorageSharedKeyCredential(ACCOUNT_NAME, 
ACCOUNT_KEY))
+                    .buildClient();
+        }
+    }
+
+    @Autowired
+    CamelContext camelContext;
+
+    @Autowired
+    ProducerTemplate template;
+
+    void uploadTestBlob() {
+        BlobServiceClient client = 
camelContext.getRegistry().lookupByNameAndType("serviceClient", 
BlobServiceClient.class);
+        client.createBlobContainerIfNotExists(CONTAINER_NAME);
+
+        byte[] content = ORIGINAL_CONTENT.getBytes(StandardCharsets.UTF_8);
+        template.send("direct:uploadBlob", exchange -> {
+            exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+            exchange.getIn().setBody(new ByteArrayInputStream(content));
+        });
+    }
+
+    @Test
+    void testCreateSnapshotReturnsId() {
+        uploadTestBlob();
+
+        Exchange result = template.send("direct:createSnapshot", exchange -> {
+            exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+        });
+        assertNull(result.getException(), "createSnapshot should succeed: " + 
result.getException());
+
+        String snapshotId = 
result.getMessage().getHeader(BlobConstants.BLOB_SNAPSHOT_ID, String.class);
+        assertNotNull(snapshotId, "Snapshot ID should be returned in header");
+    }
+
+    @Test
+    void testReadBlobViaSnapshotId() {
+        uploadTestBlob();
+
+        Exchange snapshotResult = template.send("direct:createSnapshot", 
exchange -> {
+            exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+        });
+        String snapshotId = 
snapshotResult.getMessage().getHeader(BlobConstants.BLOB_SNAPSHOT_ID, 
String.class);
+        assertNotNull(snapshotId);
+
+        String updatedContent = "modified after snapshot";
+        template.send("direct:uploadBlob", exchange -> {
+            exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+            exchange.getIn().setBody(new 
ByteArrayInputStream(updatedContent.getBytes(StandardCharsets.UTF_8)));
+        });
+
+        Exchange snapshotRead = template.send("direct:getBlob", exchange -> {
+            exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+            exchange.getIn().setHeader(BlobConstants.BLOB_SNAPSHOT_ID, 
snapshotId);
+        });
+        assertNull(snapshotRead.getException(), "Snapshot read should succeed: 
" + snapshotRead.getException());
+        String retrieved = snapshotRead.getMessage().getBody(String.class);
+        assertEquals(ORIGINAL_CONTENT, retrieved, "Snapshot should return 
original content");
+
+        Exchange liveRead = template.send("direct:getBlob", exchange -> {
+            exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+        });
+        assertNull(liveRead.getException(), "Live read should succeed: " + 
liveRead.getException());
+        String liveRetrieved = liveRead.getMessage().getBody(String.class);
+        assertEquals(updatedContent, liveRetrieved, "Live blob should return 
updated content");
+    }
+}
diff --git 
a/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobTagsTest.java
 
b/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobTagsTest.java
new file mode 100644
index 00000000..4a814249
--- /dev/null
+++ 
b/azure/camel-example-spring-boot-azure-storage-blob/src/test/java/camel/sample/BlobTagsTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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 camel.sample;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.azure.storage.common.StorageSharedKeyCredential;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.component.azure.storage.blob.BlobConstants;
+import org.apache.camel.spring.boot.CamelAutoConfiguration;
+import org.apache.camel.test.spring.junit6.CamelSpringBootTest;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@DirtiesContext
+@CamelSpringBootTest
+@SpringBootTest(classes = {
+        CamelAutoConfiguration.class,
+        BlobTagsTest.class,
+        BlobTagsTest.AzuriteConfiguration.class,
+        BlobRouteBuilder.class,
+        BlobTagsRouteBuilder.class
+})
+public class BlobTagsTest {
+
+    static final String ACCOUNT_NAME = "devstoreaccount1";
+    static final String ACCOUNT_KEY =
+            
"Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
+    static final String CONTAINER_NAME = "tagtest";
+    static final String BLOB_NAME = "test-blob.txt";
+
+    static GenericContainer<?> azurite = new 
GenericContainer<>("mcr.microsoft.com/azure-storage/azurite:3.35.0")
+            .withExposedPorts(10000)
+            .waitingFor(Wait.forListeningPort());
+
+    static {
+        azurite.start();
+    }
+
+    @DynamicPropertySource
+    static void azuriteProperties(DynamicPropertyRegistry registry) {
+        registry.add("azure.storage.account-name", () -> ACCOUNT_NAME);
+        registry.add("azure.storage.container-name", () -> CONTAINER_NAME);
+        registry.add("camel.component.azure-storage-blob.credential-type", () 
-> "SHARED_KEY_CREDENTIAL");
+    }
+
+    @Configuration
+    static class AzuriteConfiguration {
+        @Bean
+        BlobServiceClient serviceClient() {
+            String endpoint = String.format("http://%s:%d/%s";,
+                    azurite.getHost(), azurite.getMappedPort(10000), 
ACCOUNT_NAME);
+            return new BlobServiceClientBuilder()
+                    .endpoint(endpoint)
+                    .credential(new StorageSharedKeyCredential(ACCOUNT_NAME, 
ACCOUNT_KEY))
+                    .buildClient();
+        }
+    }
+
+    @Autowired
+    CamelContext camelContext;
+
+    @Autowired
+    ProducerTemplate template;
+
+    void uploadTestBlob() {
+        BlobServiceClient client = 
camelContext.getRegistry().lookupByNameAndType("serviceClient", 
BlobServiceClient.class);
+        client.createBlobContainerIfNotExists(CONTAINER_NAME);
+
+        byte[] content = "hello blob tags".getBytes(StandardCharsets.UTF_8);
+        template.send("direct:uploadBlob", exchange -> {
+            exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+            exchange.getIn().setBody(new ByteArrayInputStream(content));
+        });
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void testSetAndGetBlobTags() {
+        uploadTestBlob();
+
+        Map<String, String> tags = Map.of(
+                "status", "quarantine",
+                "category", "document",
+                "priority", "high");
+
+        Exchange setResult = template.send("direct:setBlobTags", exchange -> {
+            exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+            exchange.getIn().setHeader(BlobConstants.BLOB_TAGS, tags);
+        });
+        assertNull(setResult.getException(), "setBlobTags should succeed: " + 
setResult.getException());
+
+        Exchange result = template.send("direct:getBlobTags", exchange -> {
+            exchange.getIn().setHeader(BlobConstants.BLOB_NAME, BLOB_NAME);
+        });
+        assertNull(result.getException(), "getBlobTags should succeed: " + 
result.getException());
+
+        Map<String, String> retrieved = result.getMessage().getBody(Map.class);
+        assertNotNull(retrieved, "Tags should not be null");
+        assertEquals("quarantine", retrieved.get("status"));
+        assertEquals("document", retrieved.get("category"));
+        assertEquals("high", retrieved.get("priority"));
+    }
+}
diff --git a/azure/pom.xml b/azure/pom.xml
index f03b0c4d..b88ba661 100644
--- a/azure/pom.xml
+++ b/azure/pom.xml
@@ -63,6 +63,7 @@
   <modules>
     <module>camel-example-spring-boot-azure-servicebus</module>
     <module>camel-example-spring-boot-azure-eventhubs</module>
+    <module>camel-example-spring-boot-azure-storage-blob</module>
   </modules>
 
 </project>

Reply via email to