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