From de2c454b9774ad2b1139fef7ef107650de392496 Mon Sep 17 00:00:00 2001
From: sebo <sebastian.bergandy@external.mgm-cp.com>
Date: Mon, 7 Apr 2025 17:05:20 +0200
Subject: [PATCH 1/8] OZG-7232 switch from WebClient to RestClient

Subtask: OZG-8002
---
 .../SmartDocumentsBescheidRemoteService.java  |  32 +++--
 .../SmartDocumentsConfiguration.java          |  60 +++++----
 .../SmartDocumentsConfigurationTest.java      | 118 ++++++++++++++++++
 3 files changed, 173 insertions(+), 37 deletions(-)
 create mode 100644 document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java

diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsBescheidRemoteService.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsBescheidRemoteService.java
index 8a32e8a..02eae17 100644
--- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsBescheidRemoteService.java
+++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsBescheidRemoteService.java
@@ -25,6 +25,7 @@ package de.ozgcloud.document.bescheid.smartdocuments;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.Collection;
 import java.util.Optional;
 
@@ -34,14 +35,15 @@ import javax.xml.xpath.XPathConstants;
 import javax.xml.xpath.XPathExpressionException;
 import javax.xml.xpath.XPathFactory;
 
+import org.apache.commons.io.IOUtils;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.http.HttpRequest;
 import org.springframework.http.HttpStatusCode;
 import org.springframework.http.MediaType;
+import org.springframework.http.client.ClientHttpResponse;
 import org.springframework.stereotype.Service;
-import org.springframework.web.reactive.function.BodyExtractors;
-import org.springframework.web.reactive.function.client.ClientResponse;
-import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.client.RestClient;
 import org.w3c.dom.Document;
 import org.w3c.dom.Text;
 import org.xml.sax.SAXException;
@@ -69,7 +71,6 @@ import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
-import reactor.core.publisher.Mono;
 
 @Log4j2
 @Service
@@ -87,7 +88,7 @@ class SmartDocumentsBescheidRemoteService implements BescheidRemoteService {
 	private static final MediaType JSON_MEDIA_TYPE_FOR_SD = MediaType.APPLICATION_JSON_UTF8;
 
 	@Qualifier("smartDocuments")
-	private final WebClient smartDocumentsWebClient;
+	private final RestClient smartDocumentsRestClient;
 
 	private final SmartDocumentsProperties properties;
 
@@ -96,19 +97,24 @@ class SmartDocumentsBescheidRemoteService implements BescheidRemoteService {
 		var sdRequest = createRequest(request, vorgang);
 		LOG.debug(() -> buildLogRequest(sdRequest));
 
-		return smartDocumentsWebClient.post().accept(MediaType.APPLICATION_JSON)
+		var response = smartDocumentsRestClient.post().accept(MediaType.APPLICATION_JSON)
 				.contentType(JSON_MEDIA_TYPE_FOR_SD)
-				.bodyValue(sdRequest)
+				.body(sdRequest)
 				.retrieve()
 				.onStatus(HttpStatusCode::is4xxClientError, this::handleClientError)
-				.bodyToMono(SmartDocumentsResponse.class)
-				.map(response -> buildBescheid(request, response))
-				.block();
+				.toEntity(SmartDocumentsResponse.class)
+				.getBody();
+		return buildBescheid(request, response);
+
 	}
 
-	Mono<Throwable> handleClientError(ClientResponse response) {
-		return response.body(BodyExtractors.toMono(String.class))
-				.map(content -> new TechnicalException("Client-Error: " + content));
+	void handleClientError(HttpRequest request, ClientHttpResponse response) {
+		try {
+			var responseBody = IOUtils.toString(response.getBody(), StandardCharsets.UTF_8);
+			throw new TechnicalException("Client-Error: " + response.getStatusCode() + " " + responseBody);
+		} catch (IOException e) {
+			throw new TechnicalException("Couldn't read response body", e);
+		}
 	}
 
 	private String buildLogRequest(SmartDocumentsRequest request) {
diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
index 01255a1..0462d99 100644
--- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
+++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
@@ -23,16 +23,20 @@
  */
 package de.ozgcloud.document.bescheid.smartdocuments;
 
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.CredentialsProvider;
+import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+
+import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner;
+import org.apache.hc.core5.http.HttpHost;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.http.client.reactive.ReactorClientHttpConnector;
-import org.springframework.web.reactive.function.client.ExchangeFilterFunctions;
-import org.springframework.web.reactive.function.client.WebClient;
-
-import reactor.netty.http.client.HttpClient;
-import reactor.netty.transport.ProxyProvider;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.web.client.RestClient;
 
 @Configuration
 @ConditionalOnProperty("ozgcloud.bescheid.smart-documents.url")
@@ -42,35 +46,43 @@ class SmartDocumentsConfiguration {
 	private SmartDocumentsProperties properties;
 
 	@Bean("smartDocuments")
-	WebClient smartDocumentsWebClient() {
-		ReactorClientHttpConnector connector = new ReactorClientHttpConnector(buildHttpClient());
-
-		return WebClient.builder()
+	RestClient smartDocumentsRestClient() {
+		return RestClient.builder()
+				.requestFactory(new HttpComponentsClientHttpRequestFactory(buildHttpClient()))
 				.baseUrl(properties.getUrl())
-				.filter(ExchangeFilterFunctions.basicAuthentication(properties.getBasicAuth().getUsername(), properties.getBasicAuth().getPassword()))
-				.clientConnector(connector)
+				.defaultHeaders(headers -> headers.setBasicAuth(properties.getBasicAuth().getUsername(), properties.getBasicAuth().getPassword()))
 				.build();
 	}
 
-	private HttpClient buildHttpClient() {
+	CloseableHttpClient buildHttpClient() {
 		if (properties.getProxy() != null) {
 			return createProxyHttpClient();
-		} else {
-			return createNoProxyHttpClient();
 		}
+		return createNoProxyHttpClient();
 	}
 
-	private HttpClient createNoProxyHttpClient() {
-		return HttpClient.create();
+	CloseableHttpClient createNoProxyHttpClient() {
+		return HttpClients.createDefault();
 	}
 
-	private HttpClient createProxyHttpClient() {
-		return HttpClient.create()
-				.proxy(proxy -> proxy.type(ProxyProvider.Proxy.HTTP)
-						.host(properties.getProxy().getHost())
-						.port(properties.getProxy().getPort())
-						.username(properties.getProxy().getUsername())
-						.password(username -> properties.getProxy().getPassword()));
+	CloseableHttpClient createProxyHttpClient() {
+		return HttpClients.custom()
+				.setRoutePlanner(createProxyRoutePlanner())
+				.setDefaultCredentialsProvider(createProxyCredentialsProvider())
+				.build();
+	}
+
+	DefaultProxyRoutePlanner createProxyRoutePlanner() {
+		return new DefaultProxyRoutePlanner(new HttpHost(properties.getProxy().getHost(), properties.getProxy().getPort()));
+	}
+
+	CredentialsProvider createProxyCredentialsProvider() {
+		var host = properties.getProxy().getHost();
+		var port = properties.getProxy().getPort();
+		var username = properties.getProxy().getUsername();
+		var password = properties.getProxy().getPassword().toCharArray();
+
+		return CredentialsProviderBuilder.create().add(new AuthScope(host, port), username, password).build();
 	}
 
 }
diff --git a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
new file mode 100644
index 0000000..7973937
--- /dev/null
+++ b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
@@ -0,0 +1,118 @@
+package de.ozgcloud.document.bescheid.smartdocuments;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHost;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+
+import de.ozgcloud.document.bescheid.smartdocuments.SmartDocumentsProperties.ProxyConfiguration;
+
+class SmartDocumentsConfigurationTest {
+
+	@Spy
+	@InjectMocks
+	private SmartDocumentsConfiguration smartDocumentsConfiguration;
+
+	@Mock
+	private SmartDocumentsProperties properties;
+
+	@Nested
+	class CreateProxyCredentialsProviderTest {
+
+		private final String user = "max";
+		private final String password = "max2";
+		private final String host = "test-proxy.local";
+		private final int port = 8080;
+		private final ProxyConfiguration proxyConfiguration = new ProxyConfiguration();
+
+		@BeforeEach
+		void setUp() {
+			proxyConfiguration.setHost(host);
+			proxyConfiguration.setPort(port);
+			proxyConfiguration.setUsername(user);
+			proxyConfiguration.setPassword(password);
+			when(properties.getProxy()).thenReturn(proxyConfiguration);
+		}
+
+		@Test
+		void shouldCreateWithUserAndPassword() {
+			var credentialsProvider = smartDocumentsConfiguration.createProxyCredentialsProvider();
+
+			var credentialsForProxy = (UsernamePasswordCredentials) credentialsProvider.getCredentials(new AuthScope(host, port), null);
+
+			assertThat(credentialsForProxy).extracting(
+							UsernamePasswordCredentials::getUserName,
+							UsernamePasswordCredentials::getUserPassword)
+					.containsExactly(user, password.toCharArray());
+		}
+	}
+
+	@Nested
+	class CreateProxyRoutePlannerTest {
+
+		private final String host = "test-proxy.local";
+		private final int port = 8080;
+		private final ProxyConfiguration proxyConfiguration = new ProxyConfiguration();
+
+		@BeforeEach
+		void setUp() {
+			proxyConfiguration.setHost(host);
+			proxyConfiguration.setPort(port);
+			when(properties.getProxy()).thenReturn(proxyConfiguration);
+		}
+
+		@Test
+		void shouldCreateWithProxySettings() throws HttpException {
+			var routePlanner = smartDocumentsConfiguration.createProxyRoutePlanner();
+
+			var proxyHost = routePlanner.determineRoute(new HttpHost("http://any"), new HttpClientContext()).getProxyHost();
+
+			assertThat(proxyHost).extracting(
+					HttpHost::getHostName,
+					HttpHost::getPort
+			).containsExactly(host, port);
+		}
+	}
+
+	@Nested
+	class BuildHttpClientTest {
+		private final CloseableHttpClient proxyHttpClient = HttpClients.createDefault();
+		private final CloseableHttpClient nonProxyHttpClient = HttpClients.createDefault();
+
+		@BeforeEach
+		void setUp() {
+		}
+
+		@Test
+		void shouldReturnProxyHttpClient() {
+			doReturn(proxyHttpClient).when(smartDocumentsConfiguration).createProxyHttpClient();
+			when(properties.getProxy()).thenReturn(new ProxyConfiguration());
+
+			var httpClient = smartDocumentsConfiguration.buildHttpClient();
+
+			assertThat(httpClient).isSameAs(proxyHttpClient);
+		}
+
+		@Test
+		void shouldReturnNonProxyHttpClient() {
+			doReturn(nonProxyHttpClient).when(smartDocumentsConfiguration).createNoProxyHttpClient();
+			when(properties.getProxy()).thenReturn(null);
+
+			var httpClient = smartDocumentsConfiguration.buildHttpClient();
+
+			assertThat(httpClient).isSameAs(nonProxyHttpClient);
+		}
+	}
+}
\ No newline at end of file
-- 
GitLab


From 7da9a7923db19908f52470d481e35b93e6f36ff2 Mon Sep 17 00:00:00 2001
From: sebo <sebastian.bergandy@external.mgm-cp.com>
Date: Thu, 10 Apr 2025 15:29:39 +0200
Subject: [PATCH 2/8] OZG-7232 add basic auth if configured

Subtask: OZG-8005
---
 .../SmartDocumentsConfiguration.java          | 11 +++++++-
 .../SmartDocumentsConfigurationTest.java      | 27 +++++++++++++++++++
 .../UsernamePasswordTestFactory.java          | 26 ++++++++++++++++++
 3 files changed, 63 insertions(+), 1 deletion(-)
 create mode 100644 document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/UsernamePasswordTestFactory.java

diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
index 0462d99..3ba2292 100644
--- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
+++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
@@ -23,6 +23,8 @@
  */
 package de.ozgcloud.document.bescheid.smartdocuments;
 
+import java.util.Objects;
+
 import org.apache.hc.client5.http.auth.AuthScope;
 import org.apache.hc.client5.http.auth.CredentialsProvider;
 import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
@@ -35,6 +37,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
 import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
 import org.springframework.web.client.RestClient;
 
@@ -50,7 +53,7 @@ class SmartDocumentsConfiguration {
 		return RestClient.builder()
 				.requestFactory(new HttpComponentsClientHttpRequestFactory(buildHttpClient()))
 				.baseUrl(properties.getUrl())
-				.defaultHeaders(headers -> headers.setBasicAuth(properties.getBasicAuth().getUsername(), properties.getBasicAuth().getPassword()))
+				.defaultHeaders(this::addBasicAuthenticationIfConfigured)
 				.build();
 	}
 
@@ -85,4 +88,10 @@ class SmartDocumentsConfiguration {
 		return CredentialsProviderBuilder.create().add(new AuthScope(host, port), username, password).build();
 	}
 
+	void addBasicAuthenticationIfConfigured(HttpHeaders headers) {
+		if (Objects.nonNull(properties.getBasicAuth())) {
+			headers.setBasicAuth(properties.getBasicAuth().getUsername(), properties.getBasicAuth().getPassword());
+		}
+	}
+
 }
diff --git a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
index 7973937..7d21660 100644
--- a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
+++ b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
@@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Spy;
+import org.springframework.http.HttpHeaders;
 
 import de.ozgcloud.document.bescheid.smartdocuments.SmartDocumentsProperties.ProxyConfiguration;
 
@@ -115,4 +116,30 @@ class SmartDocumentsConfigurationTest {
 			assertThat(httpClient).isSameAs(nonProxyHttpClient);
 		}
 	}
+
+	@Nested
+	class AddBasicAuthenticationIfConfiguredTest {
+
+		@Test
+		void shouldNotAddBasicAuthentication() {
+			when(properties.getBasicAuth()).thenReturn(null);
+			var headers = new HttpHeaders();
+
+			smartDocumentsConfiguration.addBasicAuthenticationIfConfigured(headers);
+
+			assertThat(headers.get(HttpHeaders.AUTHORIZATION)).isNull();
+		}
+
+		@Test
+		void shouldAddBasicAuthentication() {
+			when(properties.getBasicAuth()).thenReturn(UsernamePasswordTestFactory.create());
+			var headers = new HttpHeaders();
+
+			smartDocumentsConfiguration.addBasicAuthenticationIfConfigured(headers);
+
+			assertThat(headers.get(HttpHeaders.AUTHORIZATION))
+					.isNotNull()
+					.containsExactly(UsernamePasswordTestFactory.getAsBasicAuthenticationHeader());
+		}
+	}
 }
\ No newline at end of file
diff --git a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/UsernamePasswordTestFactory.java b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/UsernamePasswordTestFactory.java
new file mode 100644
index 0000000..7ef947a
--- /dev/null
+++ b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/UsernamePasswordTestFactory.java
@@ -0,0 +1,26 @@
+package de.ozgcloud.document.bescheid.smartdocuments;
+
+import org.bouncycastle.util.encoders.Base64;
+
+import de.ozgcloud.document.bescheid.smartdocuments.SmartDocumentsProperties.UsernamePassword;
+
+public class UsernamePasswordTestFactory {
+
+	public static final String USERNAME = "max";
+	public static final String PASSWORD = "max123";
+
+	public static UsernamePassword create() {
+		var usernamePassword = new UsernamePassword();
+		usernamePassword.setUsername(USERNAME);
+		usernamePassword.setPassword(PASSWORD);
+		return usernamePassword;
+	}
+
+	public static String getAsBasicAuthenticationHeader() {
+		var base64EncodedAuth = new String(Base64.encode(
+				(UsernamePasswordTestFactory.USERNAME + ":" + UsernamePasswordTestFactory.PASSWORD).getBytes()));
+
+		return "Basic " + base64EncodedAuth;
+	}
+
+}
-- 
GitLab


From d2562eea1b82d91ac46ce6f967faabed01adaad9 Mon Sep 17 00:00:00 2001
From: sebo <sebastian.bergandy@external.mgm-cp.com>
Date: Fri, 11 Apr 2025 14:01:29 +0200
Subject: [PATCH 3/8] OZG-7232 add tls

Subtask: OZG-8005
---
 .../SmartDocumentsConfiguration.java          |  34 +++++-
 .../SmartDocumentsProperties.java             |   6 +-
 .../SmartDocumentsConfigurationTest.java      | 109 +++++++++++++++++-
 3 files changed, 139 insertions(+), 10 deletions(-)

diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
index 3ba2292..74220c5 100644
--- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
+++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
@@ -29,12 +29,18 @@ import org.apache.hc.client5.http.auth.AuthScope;
 import org.apache.hc.client5.http.auth.CredentialsProvider;
 import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
 import org.apache.hc.client5.http.impl.classic.HttpClients;
 
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
 import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner;
+import org.apache.hc.client5.http.io.HttpClientConnectionManager;
+import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
 import org.apache.hc.core5.http.HttpHost;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.ssl.NoSuchSslBundleException;
+import org.springframework.boot.ssl.SslBundles;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpHeaders;
@@ -47,6 +53,8 @@ class SmartDocumentsConfiguration {
 
 	@Autowired
 	private SmartDocumentsProperties properties;
+	@Autowired
+	private SslBundles sslBundles;
 
 	@Bean("smartDocuments")
 	RestClient smartDocumentsRestClient() {
@@ -59,17 +67,22 @@ class SmartDocumentsConfiguration {
 
 	CloseableHttpClient buildHttpClient() {
 		if (properties.getProxy() != null) {
-			return createProxyHttpClient();
+			return createHttpClientUsingProxy();
 		}
 		return createNoProxyHttpClient();
 	}
 
+	HttpClientBuilder createDefulatHttpClientBuilder() {
+		return HttpClients.custom()
+				.setConnectionManager(createConnectionManagerWithClientCertificateIfConfigured());
+	}
+
 	CloseableHttpClient createNoProxyHttpClient() {
-		return HttpClients.createDefault();
+		return createDefulatHttpClientBuilder().build();
 	}
 
-	CloseableHttpClient createProxyHttpClient() {
-		return HttpClients.custom()
+	CloseableHttpClient createHttpClientUsingProxy() {
+		return createDefulatHttpClientBuilder()
 				.setRoutePlanner(createProxyRoutePlanner())
 				.setDefaultCredentialsProvider(createProxyCredentialsProvider())
 				.build();
@@ -94,4 +107,17 @@ class SmartDocumentsConfiguration {
 		}
 	}
 
+	HttpClientConnectionManager createConnectionManagerWithClientCertificateIfConfigured() {
+		var builder = PoolingHttpClientConnectionManagerBuilder.create();
+		try {
+			var sslBundleName = properties.getSslBundleName();
+			if (Objects.nonNull(sslBundleName)) {
+				builder.setTlsSocketStrategy(new DefaultClientTlsStrategy(sslBundles.getBundle(sslBundleName).createSslContext()));
+			}
+			return builder.build();
+		} catch (NoSuchSslBundleException e) {
+			return builder.build();
+		}
+	}
+
 }
diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsProperties.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsProperties.java
index 6b0d37b..1c7839b 100644
--- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsProperties.java
+++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsProperties.java
@@ -52,10 +52,14 @@ public class SmartDocumentsProperties {
 	/**
 	 * Credential for basic auth to the Smart Documents Server
 	 */
-	@NotNull
 	@Valid
 	private UsernamePassword basicAuth;
 
+	/**
+	 * Name of SSL bundle need for client certificate authentication.
+	 */
+	private String sslBundleName;
+
 	/**
 	 * Smart Documents Template Group
 	 */
diff --git a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
index 7d21660..3d18296 100644
--- a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
+++ b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
@@ -3,19 +3,28 @@ package de.ozgcloud.document.bescheid.smartdocuments;
 import static org.assertj.core.api.Assertions.*;
 import static org.mockito.Mockito.*;
 
+import javax.net.ssl.SSLContext;
+
 import org.apache.hc.client5.http.auth.AuthScope;
 import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.HttpHost;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
+import org.mockito.MockedStatic;
 import org.mockito.Spy;
+import org.springframework.boot.ssl.NoSuchSslBundleException;
+import org.springframework.boot.ssl.SslBundle;
+import org.springframework.boot.ssl.SslBundles;
 import org.springframework.http.HttpHeaders;
 
 import de.ozgcloud.document.bescheid.smartdocuments.SmartDocumentsProperties.ProxyConfiguration;
@@ -28,9 +37,11 @@ class SmartDocumentsConfigurationTest {
 
 	@Mock
 	private SmartDocumentsProperties properties;
+	@Mock
+	private SslBundles sslBundles;
 
 	@Nested
-	class CreateProxyCredentialsProviderTest {
+	class TestCreateProxyCredentialsProvider {
 
 		private final String user = "max";
 		private final String password = "max2";
@@ -61,7 +72,7 @@ class SmartDocumentsConfigurationTest {
 	}
 
 	@Nested
-	class CreateProxyRoutePlannerTest {
+	class TestCreateProxyRoutePlanner {
 
 		private final String host = "test-proxy.local";
 		private final int port = 8080;
@@ -88,7 +99,7 @@ class SmartDocumentsConfigurationTest {
 	}
 
 	@Nested
-	class BuildHttpClientTest {
+	class TestBuildHttpClient {
 		private final CloseableHttpClient proxyHttpClient = HttpClients.createDefault();
 		private final CloseableHttpClient nonProxyHttpClient = HttpClients.createDefault();
 
@@ -98,7 +109,7 @@ class SmartDocumentsConfigurationTest {
 
 		@Test
 		void shouldReturnProxyHttpClient() {
-			doReturn(proxyHttpClient).when(smartDocumentsConfiguration).createProxyHttpClient();
+			doReturn(proxyHttpClient).when(smartDocumentsConfiguration).createHttpClientUsingProxy();
 			when(properties.getProxy()).thenReturn(new ProxyConfiguration());
 
 			var httpClient = smartDocumentsConfiguration.buildHttpClient();
@@ -118,7 +129,7 @@ class SmartDocumentsConfigurationTest {
 	}
 
 	@Nested
-	class AddBasicAuthenticationIfConfiguredTest {
+	class TestAddBasicAuthenticationIfConfigured {
 
 		@Test
 		void shouldNotAddBasicAuthentication() {
@@ -142,4 +153,92 @@ class SmartDocumentsConfigurationTest {
 					.containsExactly(UsernamePasswordTestFactory.getAsBasicAuthenticationHeader());
 		}
 	}
+
+	@Nested
+	class TestCreateConnectionManagerWithClientCertificateIfConfigured {
+
+		private MockedStatic<PoolingHttpClientConnectionManagerBuilder> staticBuilder;
+		@Mock
+		private PoolingHttpClientConnectionManagerBuilder builder;
+		@Mock
+		private SslBundle sslBundle;
+		@Mock
+		private SSLContext sslContext;
+
+		private final String bundleName = "smartdocuments_bundle";
+		private final PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
+
+		@BeforeEach
+		void setUp() {
+			staticBuilder = mockStatic(PoolingHttpClientConnectionManagerBuilder.class);
+			staticBuilder.when(PoolingHttpClientConnectionManagerBuilder::create).thenReturn(builder);
+			when(builder.build()).thenReturn(connectionManager);
+		}
+
+		@AfterEach
+		void tearDown() {
+			staticBuilder.close();
+		}
+
+		@Test
+		void shouldCreateWithBuilder() {
+			smartDocumentsConfiguration.createConnectionManagerWithClientCertificateIfConfigured();
+
+			staticBuilder.verify(() -> PoolingHttpClientConnectionManagerBuilder.create());
+		}
+
+		@Nested
+		class OnMissingClientCertificateConfiguration {
+
+			@Test
+			void shouldCreateWithoutTlsStrategyOnMissingBundleName() {
+				when(properties.getSslBundleName()).thenReturn(null);
+
+				var created = smartDocumentsConfiguration.createConnectionManagerWithClientCertificateIfConfigured();
+
+				verify(builder, never()).setTlsSocketStrategy(any());
+				assertThat(created).isSameAs(connectionManager);
+
+			}
+
+			@Test
+			void shouldCreateWithoutTlsStrategyOnMissingBundleConfiguration() {
+				when(properties.getSslBundleName()).thenReturn(bundleName);
+				when(sslBundles.getBundle(anyString())).thenThrow(new NoSuchSslBundleException("some_bundle", "error message"));
+
+				var created = smartDocumentsConfiguration.createConnectionManagerWithClientCertificateIfConfigured();
+
+				verify(builder, never()).setTlsSocketStrategy(any());
+				assertThat(created).isSameAs(connectionManager);
+			}
+
+		}
+
+		@Nested
+		class OnExistingClientCertificateConfiguration {
+
+			@BeforeEach
+			void setUp() {
+				when(properties.getSslBundleName()).thenReturn(bundleName);
+				when(sslBundles.getBundle(any())).thenReturn(sslBundle);
+				when(sslBundle.createSslContext()).thenReturn(sslContext);
+			}
+
+			@Test
+			void shouldGetSslBundle() {
+				smartDocumentsConfiguration.createConnectionManagerWithClientCertificateIfConfigured();
+
+				verify(sslBundles).getBundle(bundleName);
+			}
+
+			@Test
+			void shouldCreateWithTlsStrategy() {
+				var created = smartDocumentsConfiguration.createConnectionManagerWithClientCertificateIfConfigured();
+
+				verify(builder).setTlsSocketStrategy(any());
+				assertThat(created).isSameAs(connectionManager);
+			}
+		}
+
+	}
 }
\ No newline at end of file
-- 
GitLab


From 3c5d91e055dbadcd00161b6db586dd25d45baf89 Mon Sep 17 00:00:00 2001
From: sebo <sebastian.bergandy@external.mgm-cp.com>
Date: Mon, 14 Apr 2025 09:09:39 +0200
Subject: [PATCH 4/8] OZG-7232 switch to constructor injection

---
 .../smartdocuments/SmartDocumentsConfiguration.java   | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
index 74220c5..9716415 100644
--- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
+++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
@@ -31,13 +31,11 @@ import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
 import org.apache.hc.client5.http.impl.classic.HttpClients;
-
 import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
 import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner;
 import org.apache.hc.client5.http.io.HttpClientConnectionManager;
 import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
 import org.apache.hc.core5.http.HttpHost;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.ssl.NoSuchSslBundleException;
 import org.springframework.boot.ssl.SslBundles;
@@ -47,14 +45,15 @@ import org.springframework.http.HttpHeaders;
 import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
 import org.springframework.web.client.RestClient;
 
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
 @Configuration
 @ConditionalOnProperty("ozgcloud.bescheid.smart-documents.url")
 class SmartDocumentsConfiguration {
 
-	@Autowired
-	private SmartDocumentsProperties properties;
-	@Autowired
-	private SslBundles sslBundles;
+	private final SmartDocumentsProperties properties;
+	private final SslBundles sslBundles;
 
 	@Bean("smartDocuments")
 	RestClient smartDocumentsRestClient() {
-- 
GitLab


From 8e6515dc9a6762491de9b3e8f0b6a3427feb0323 Mon Sep 17 00:00:00 2001
From: sebo <sebastian.bergandy@external.mgm-cp.com>
Date: Mon, 14 Apr 2025 09:13:16 +0200
Subject: [PATCH 5/8] OZG-7232 change tests order

---
 .../SmartDocumentsConfigurationTest.java      | 74 +++++++++----------
 1 file changed, 37 insertions(+), 37 deletions(-)

diff --git a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
index 3d18296..c5d27ad 100644
--- a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
+++ b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
@@ -41,33 +41,32 @@ class SmartDocumentsConfigurationTest {
 	private SslBundles sslBundles;
 
 	@Nested
-	class TestCreateProxyCredentialsProvider {
-
-		private final String user = "max";
-		private final String password = "max2";
-		private final String host = "test-proxy.local";
-		private final int port = 8080;
-		private final ProxyConfiguration proxyConfiguration = new ProxyConfiguration();
+	class TestBuildHttpClient {
+		private final CloseableHttpClient proxyHttpClient = HttpClients.createDefault();
+		private final CloseableHttpClient nonProxyHttpClient = HttpClients.createDefault();
 
 		@BeforeEach
 		void setUp() {
-			proxyConfiguration.setHost(host);
-			proxyConfiguration.setPort(port);
-			proxyConfiguration.setUsername(user);
-			proxyConfiguration.setPassword(password);
-			when(properties.getProxy()).thenReturn(proxyConfiguration);
 		}
 
 		@Test
-		void shouldCreateWithUserAndPassword() {
-			var credentialsProvider = smartDocumentsConfiguration.createProxyCredentialsProvider();
+		void shouldReturnProxyHttpClient() {
+			doReturn(proxyHttpClient).when(smartDocumentsConfiguration).createHttpClientUsingProxy();
+			when(properties.getProxy()).thenReturn(new ProxyConfiguration());
 
-			var credentialsForProxy = (UsernamePasswordCredentials) credentialsProvider.getCredentials(new AuthScope(host, port), null);
+			var httpClient = smartDocumentsConfiguration.buildHttpClient();
 
-			assertThat(credentialsForProxy).extracting(
-							UsernamePasswordCredentials::getUserName,
-							UsernamePasswordCredentials::getUserPassword)
-					.containsExactly(user, password.toCharArray());
+			assertThat(httpClient).isSameAs(proxyHttpClient);
+		}
+
+		@Test
+		void shouldReturnNonProxyHttpClient() {
+			doReturn(nonProxyHttpClient).when(smartDocumentsConfiguration).createNoProxyHttpClient();
+			when(properties.getProxy()).thenReturn(null);
+
+			var httpClient = smartDocumentsConfiguration.buildHttpClient();
+
+			assertThat(httpClient).isSameAs(nonProxyHttpClient);
 		}
 	}
 
@@ -99,32 +98,33 @@ class SmartDocumentsConfigurationTest {
 	}
 
 	@Nested
-	class TestBuildHttpClient {
-		private final CloseableHttpClient proxyHttpClient = HttpClients.createDefault();
-		private final CloseableHttpClient nonProxyHttpClient = HttpClients.createDefault();
+	class TestCreateProxyCredentialsProvider {
+
+		private final String user = "max";
+		private final String password = "max2";
+		private final String host = "test-proxy.local";
+		private final int port = 8080;
+		private final ProxyConfiguration proxyConfiguration = new ProxyConfiguration();
 
 		@BeforeEach
 		void setUp() {
+			proxyConfiguration.setHost(host);
+			proxyConfiguration.setPort(port);
+			proxyConfiguration.setUsername(user);
+			proxyConfiguration.setPassword(password);
+			when(properties.getProxy()).thenReturn(proxyConfiguration);
 		}
 
 		@Test
-		void shouldReturnProxyHttpClient() {
-			doReturn(proxyHttpClient).when(smartDocumentsConfiguration).createHttpClientUsingProxy();
-			when(properties.getProxy()).thenReturn(new ProxyConfiguration());
-
-			var httpClient = smartDocumentsConfiguration.buildHttpClient();
-
-			assertThat(httpClient).isSameAs(proxyHttpClient);
-		}
-
-		@Test
-		void shouldReturnNonProxyHttpClient() {
-			doReturn(nonProxyHttpClient).when(smartDocumentsConfiguration).createNoProxyHttpClient();
-			when(properties.getProxy()).thenReturn(null);
+		void shouldCreateWithUserAndPassword() {
+			var credentialsProvider = smartDocumentsConfiguration.createProxyCredentialsProvider();
 
-			var httpClient = smartDocumentsConfiguration.buildHttpClient();
+			var credentialsForProxy = (UsernamePasswordCredentials) credentialsProvider.getCredentials(new AuthScope(host, port), null);
 
-			assertThat(httpClient).isSameAs(nonProxyHttpClient);
+			assertThat(credentialsForProxy).extracting(
+							UsernamePasswordCredentials::getUserName,
+							UsernamePasswordCredentials::getUserPassword)
+					.containsExactly(user, password.toCharArray());
 		}
 	}
 
-- 
GitLab


From a887b4a361c96160fc945cd7c8c212eeefbe8c70 Mon Sep 17 00:00:00 2001
From: sebo <sebastian.bergandy@external.mgm-cp.com>
Date: Mon, 14 Apr 2025 09:14:09 +0200
Subject: [PATCH 6/8] OZG-7232 remove unused code

---
 .../smartdocuments/SmartDocumentsConfigurationTest.java       | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
index c5d27ad..a34c9da 100644
--- a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
+++ b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfigurationTest.java
@@ -45,10 +45,6 @@ class SmartDocumentsConfigurationTest {
 		private final CloseableHttpClient proxyHttpClient = HttpClients.createDefault();
 		private final CloseableHttpClient nonProxyHttpClient = HttpClients.createDefault();
 
-		@BeforeEach
-		void setUp() {
-		}
-
 		@Test
 		void shouldReturnProxyHttpClient() {
 			doReturn(proxyHttpClient).when(smartDocumentsConfiguration).createHttpClientUsingProxy();
-- 
GitLab


From 0fa869f7a4d0eefe0b509386faa51e2d176e77e3 Mon Sep 17 00:00:00 2001
From: sebo <sebastian.bergandy@external.mgm-cp.com>
Date: Mon, 14 Apr 2025 09:14:53 +0200
Subject: [PATCH 7/8] OZG-7232 fix typo and add change access modifier

---
 .../smartdocuments/SmartDocumentsConfiguration.java         | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
index 9716415..206c12c 100644
--- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
+++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
@@ -71,17 +71,17 @@ class SmartDocumentsConfiguration {
 		return createNoProxyHttpClient();
 	}
 
-	HttpClientBuilder createDefulatHttpClientBuilder() {
+	private HttpClientBuilder createDefaultHttpClientBuilder() {
 		return HttpClients.custom()
 				.setConnectionManager(createConnectionManagerWithClientCertificateIfConfigured());
 	}
 
 	CloseableHttpClient createNoProxyHttpClient() {
-		return createDefulatHttpClientBuilder().build();
+		return createDefaultHttpClientBuilder().build();
 	}
 
 	CloseableHttpClient createHttpClientUsingProxy() {
-		return createDefulatHttpClientBuilder()
+		return createDefaultHttpClientBuilder()
 				.setRoutePlanner(createProxyRoutePlanner())
 				.setDefaultCredentialsProvider(createProxyCredentialsProvider())
 				.build();
-- 
GitLab


From 3b5827bb55474776eb0b058261fb9a70964eb6fb Mon Sep 17 00:00:00 2001
From: sebo <sebastian.bergandy@external.mgm-cp.com>
Date: Mon, 14 Apr 2025 09:23:24 +0200
Subject: [PATCH 8/8] OZG-7232 change access modifier

---
 .../bescheid/smartdocuments/SmartDocumentsConfiguration.java    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
index 206c12c..6e141bb 100644
--- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
+++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/smartdocuments/SmartDocumentsConfiguration.java
@@ -91,7 +91,7 @@ class SmartDocumentsConfiguration {
 		return new DefaultProxyRoutePlanner(new HttpHost(properties.getProxy().getHost(), properties.getProxy().getPort()));
 	}
 
-	CredentialsProvider createProxyCredentialsProvider() {
+	private CredentialsProvider createProxyCredentialsProvider() {
 		var host = properties.getProxy().getHost();
 		var port = properties.getProxy().getPort();
 		var username = properties.getProxy().getUsername();
-- 
GitLab