From c558e718adf61f5ee0f34fc47be8877e1eaebee6 Mon Sep 17 00:00:00 2001 From: Jan Zickermann <jan.zickermann@dataport.de> Date: Fri, 22 Nov 2024 15:45:05 +0100 Subject: [PATCH] #3 OZG-7112 webclient: Add RemoteITCase using http-proxy --- .../osiv2/OsiPostfachRemoteService.java | 7 +- .../osiv2/config/OsiPostfachProperties.java | 55 +++++++++++++++ .../osiv2/config/WebClientConfiguration.java | 65 ++++++++++------- src/main/resources/application.yml | 4 +- .../osiv2/OsiPostfachRemoteServiceITCase.java | 44 +++--------- .../OsiPostfachRemoteServiceRemoteITCase.java | 69 +++++++++++++++++++ .../extension/OsiMockServerExtension.java | 44 +++++++++++- 7 files changed, 224 insertions(+), 64 deletions(-) create mode 100644 src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/OsiPostfachProperties.java create mode 100644 src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceRemoteITCase.java diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteService.java index 3e7704c..c26429d 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteService.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteService.java @@ -3,13 +3,17 @@ package de.ozgcloud.nachrichten.postfach.osiv2; import java.util.stream.Stream; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; import de.ozgcloud.nachrichten.postfach.PostfachRemoteService; +import lombok.extern.log4j.Log4j2; @Service +@ConditionalOnProperty("ozgcloud.osiv2-postfach.enabled") +@Log4j2 public record OsiPostfachRemoteService( @Qualifier("osi2PostfachWebClient") WebClient webClient ) implements PostfachRemoteService { @@ -18,9 +22,10 @@ public record OsiPostfachRemoteService( @Override public void sendMessage(PostfachNachricht nachricht) { webClient.get() - .uri("/dummy") + .uri("/_metrics") .retrieve() .bodyToMono(String.class) + .doOnNext(metricsString -> LOG.info("Metrics: {}", metricsString)) .block(); // TODO } diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/OsiPostfachProperties.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/OsiPostfachProperties.java new file mode 100644 index 0000000..44fd8ef --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/OsiPostfachProperties.java @@ -0,0 +1,55 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.config; + +import jakarta.annotation.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = OsiPostfachProperties.PREFIX) +public class OsiPostfachProperties { + + static final String PREFIX = "ozgcloud.osiv2-postfach"; + + private boolean enabled; + + @Getter + @Setter + @Configuration + @ConfigurationProperties(prefix = ApiConfiguration.PREFIX) + static class ApiConfiguration { + static final String PREFIX = OsiPostfachProperties.PREFIX + ".api"; + + private String resource; + private String url; + private String tenant; + private String nameIdentifier; + } + + /** + * HTTP proxy configuration. To use basic HTTP authentication configure username and password. (See <a + * href="https://github.com/reactor/reactor-netty/blob/3b74fcf2f5b2bdd129636e9c55608f0187ef7bd4/reactor-netty-core/src/main/java/reactor/netty/transport/ProxyProvider.java#L142">ProxyProvider</a>) + */ + @Getter + @Setter + @Configuration + @ConfigurationProperties(prefix = ProxyConfiguration.PREFIX) + static class ProxyConfiguration { + static final String PREFIX = OsiPostfachProperties.PREFIX + ".http-proxy"; + + private boolean enabled; + + private String host; + private Integer port; + + @Nullable + private String username = null; + @Nullable + private String password = null; + } +} diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/WebClientConfiguration.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/WebClientConfiguration.java index f851b78..7faa48b 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/WebClientConfiguration.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/config/WebClientConfiguration.java @@ -1,11 +1,9 @@ package de.ozgcloud.nachrichten.postfach.osiv2.config; -import java.util.Objects; - +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.core.env.Environment; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider; @@ -18,28 +16,41 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.reactive.function.client.WebClient; import lombok.RequiredArgsConstructor; +import reactor.netty.http.client.HttpClient; +import reactor.netty.transport.ProxyProvider; @Configuration @RequiredArgsConstructor +@ConditionalOnProperty("ozgcloud.osiv2-postfach.enabled") public class WebClientConfiguration { - private final Environment environment; + private final OsiPostfachProperties.ApiConfiguration apiConfiguration; + private final OsiPostfachProperties.ProxyConfiguration proxyConfiguration; @Bean("osi2PostfachWebClient") public WebClient osi2PostfachWebClient( - ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction) { - var url = Objects.requireNonNull( - environment.getProperty("ozgcloud.osiv2-postfach.api.url"), - "ozgcloud.osiv2-postfach.api.url is not set"); + ReactiveClientRegistrationRepository clientRegistrations) { return WebClient.builder() - .baseUrl(url) - .filter(serverOAuth2AuthorizedClientExchangeFilterFunction) + .baseUrl(apiConfiguration.getUrl()) + .clientConnector(new ReactorClientHttpConnector(httpClient())) + .filter(serverOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations)) .build(); } - @Bean - @Primary - ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction( + @SuppressWarnings("ConstantConditions") + private HttpClient httpClient() { + var webClient = HttpClient.create(); + return proxyConfiguration.isEnabled() ? webClient + .proxy(proxy -> proxy + .type(ProxyProvider.Proxy.HTTP) + .host(proxyConfiguration.getHost()) + .port(proxyConfiguration.getPort()) + .username(proxyConfiguration.getUsername()) + .password(username -> proxyConfiguration.getPassword()) + ) : webClient; + } + + private ServerOAuth2AuthorizedClientExchangeFilterFunction serverOAuth2AuthorizedClientExchangeFilterFunction( ReactiveClientRegistrationRepository clientRegistrations) { var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager(clientRegistrations)); @@ -47,7 +58,7 @@ public class WebClientConfiguration { return oauth; } - AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager( + private AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager( ReactiveClientRegistrationRepository clientRegistrations) { var clientService = new InMemoryReactiveOAuth2AuthorizedClientService( clientRegistrations); @@ -59,28 +70,34 @@ public class WebClientConfiguration { return authorizedClientManager; } - ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider() { + private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider() { return ReactiveOAuth2AuthorizedClientProviderBuilder.builder() .clientCredentials(builder -> - builder.accessTokenResponseClient(getClientCredentialsTokenResponseClient()) + builder.accessTokenResponseClient(clientCredentialsTokenResponseClient()) ) .build(); } - WebClientReactiveClientCredentialsTokenResponseClient getClientCredentialsTokenResponseClient() { + private WebClientReactiveClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient() { var client = new WebClientReactiveClientCredentialsTokenResponseClient(); - var resource = Objects.requireNonNull( - environment.getProperty("ozgcloud.osiv2-postfach.api.resource"), - "ozgcloud.osiv2-postfach.api.resource is not set" - ); + configureHttpClientForTokenRequests(client); + configureParametersForTokenRequests(client); + return client; + } + + private void configureHttpClientForTokenRequests(WebClientReactiveClientCredentialsTokenResponseClient client) { + client.setWebClient(WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient())) + .build()); + } + private void configureParametersForTokenRequests(WebClientReactiveClientCredentialsTokenResponseClient client) { client.addParametersConverter(source -> { MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(); // Pass a resource indicator parameter https://datatracker.ietf.org/doc/html/rfc8707 - parameters.add("resource", resource); + parameters.add("resource", apiConfiguration.getResource()); return parameters; }); - return client; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f7459cb..172d0ad 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,7 +10,7 @@ spring: osi2: client-id: 'OZG-Kopfstelle' client-secret: 'changeme' - scope: default, access_urn:some:scope:for:ozgkopfstelle + scope: default, access_urn:dataport:osi:sh:stage:ozgkopfstelle authorization-grant-type: 'client_credentials' client-authentication-method: client_secret_post provider: @@ -25,6 +25,6 @@ ozgcloud: tenant: 'SH' name-identifier: 'ozgkopfstelle' http-proxy: + enabled: true host: 127.0.0.1 port: 3128 - authentication-required: false diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java index d622702..c1f97ce 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java @@ -3,11 +3,8 @@ package de.ozgcloud.nachrichten.postfach.osiv2; import static de.ozgcloud.nachrichten.postfach.osiv2.factory.JwtFactory.*; import static org.assertj.core.api.Assertions.*; import static org.mockserver.matchers.Times.*; -import static org.mockserver.model.Header.*; import static org.mockserver.model.HttpRequest.*; import static org.mockserver.model.HttpResponse.*; -import static org.mockserver.model.Parameter.*; -import static org.mockserver.model.ParameterBody.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -17,16 +14,21 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.mockserver.client.MockServerClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.TestPropertySource; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; import de.ozgcloud.nachrichten.postfach.osiv2.extension.Jwt; import de.ozgcloud.nachrichten.postfach.osiv2.extension.OsiMockServerExtension; -import de.ozgcloud.nachrichten.postfach.osiv2.factory.JwtFactory; import lombok.SneakyThrows; @SpringBootTest(classes = TestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@ActiveProfiles("itcase") +@TestPropertySource(properties = { + "ozgcloud.osiv2-postfach.http-proxy.enabled=false", +}) public class OsiPostfachRemoteServiceITCase { @RegisterExtension @@ -55,36 +57,6 @@ public class OsiPostfachRemoteServiceITCase { @SneakyThrows public void setup() { mockClient = OSI_MOCK_SERVER_EXTENSION.getMockClient(); - - mockClient - .when( - request() - .withMethod("POST") - .withPath("/access-token") - .withHeaders( - header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") - ) - .withBody( - params( - param("grant_type", "client_credentials"), - param("client_id", CLIENT_ID), - param("client_secret", "changeme"), - param("scope", String.join(" ", CLIENT_SCOPES)), - param("resource", RESOURCE_URN) - ) - ), - exactly(1) - ) - .respond( - response() - .withStatusCode(200) - .withHeader("Content-Type", "application/json") - .withBody( - JwtFactory.createTokenResponse( - JwtFactory.createAccessTokenExampleWithExpireIn(900) - ) - ) - ); } @DisplayName("send message") @@ -98,7 +70,7 @@ public class OsiPostfachRemoteServiceITCase { .when( request() .withMethod("GET") - .withPath("/dummy"), + .withPath("/_metrics"), exactly(1) ) .respond( @@ -111,7 +83,7 @@ public class OsiPostfachRemoteServiceITCase { var requests = mockClient.retrieveRecordedRequests( request() .withMethod("GET") - .withPath("/dummy") + .withPath("/_metrics") ); assertThat(requests).hasSize(1); var jwt = Jwt.parseAuthHeaderValue( diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceRemoteITCase.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceRemoteITCase.java new file mode 100644 index 0000000..b85d522 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceRemoteITCase.java @@ -0,0 +1,69 @@ +package de.ozgcloud.nachrichten.postfach.osiv2; + +import static org.assertj.core.api.Assertions.*; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import de.ozgcloud.nachrichten.postfach.PostfachNachricht; + +@SpringBootTest(classes = TestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@ActiveProfiles("itcase") +@EnabledIfEnvironmentVariable(named = "SH_STAGE_CLIENT_SECRET", matches = ".+") +public class OsiPostfachRemoteServiceRemoteITCase { + + @Autowired + private OsiPostfachRemoteService osiPostfachRemoteService; + + private static final String MESSAGE_ID = "message-id"; + private final PostfachNachricht postfachNachricht = PostfachNachricht.builder() + .messageId(MESSAGE_ID) + .build(); + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) { + registry.add( + "spring.security.oauth2.client.registration.osi2.client-secret", + () -> System.getenv("SH_STAGE_CLIENT_SECRET") + ); + registry.add( + "ozgcloud.osiv2-postfach.http-proxy.host", + () -> matchProxyRegex(System.getenv("HTTP_PROXY")).group(1) + ); + registry.add( + "ozgcloud.osiv2-postfach.http-proxy.port", + () -> matchProxyRegex(System.getenv("HTTP_PROXY")).group(2) + ); + } + + private static Matcher matchProxyRegex(String text) { + var matcher = Pattern.compile("([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+):([0-9]+)").matcher(text); + if (matcher.find()) { + return matcher; + } + throw new IllegalArgumentException("Proxy host and port not found in '%s'".formatted(text)); + } + + @DisplayName("send message") + @Nested + class TestSendMessage { + + @DisplayName("should not fail") + @Test + void shouldNotFail() { + assertThatCode(() -> osiPostfachRemoteService.sendMessage(postfachNachricht)) + .doesNotThrowAnyException(); + } + + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/OsiMockServerExtension.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/OsiMockServerExtension.java index 08835b5..1c745f5 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/OsiMockServerExtension.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/extension/OsiMockServerExtension.java @@ -1,14 +1,24 @@ package de.ozgcloud.nachrichten.postfach.osiv2.extension; +import static de.ozgcloud.nachrichten.postfach.osiv2.factory.JwtFactory.*; +import static org.mockserver.matchers.Times.*; +import static org.mockserver.model.Header.*; +import static org.mockserver.model.HttpRequest.*; +import static org.mockserver.model.HttpResponse.*; +import static org.mockserver.model.Parameter.*; +import static org.mockserver.model.ParameterBody.*; + import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.mockserver.client.MockServerClient; import org.testcontainers.containers.MockServerContainer; import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.utility.DockerImageName; +import de.ozgcloud.nachrichten.postfach.osiv2.factory.JwtFactory; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -16,7 +26,7 @@ import lombok.extern.log4j.Log4j2; @Log4j2 @Getter @RequiredArgsConstructor -public class OsiMockServerExtension implements BeforeAllCallback, AfterAllCallback, AfterEachCallback { +public class OsiMockServerExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback { private MockServerClient mockClient; private final MockServerContainer mockServerContainer = new MockServerContainer(DockerImageName.parse("mockserver/mockserver") @@ -66,4 +76,36 @@ public class OsiMockServerExtension implements BeforeAllCallback, AfterAllCallba return "http://" + mockClient.remoteAddress().getHostName() + ":" + mockClient.remoteAddress().getPort(); } + @Override + public void beforeEach(ExtensionContext context) { + mockClient + .when( + request() + .withMethod("POST") + .withPath("/access-token") + .withHeaders( + header("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") + ) + .withBody( + params( + param("grant_type", "client_credentials"), + param("client_id", CLIENT_ID), + param("client_secret", "changeme"), + param("scope", String.join(" ", CLIENT_SCOPES)), + param("resource", RESOURCE_URN) + ) + ), + exactly(1) + ) + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "application/json") + .withBody( + JwtFactory.createTokenResponse( + JwtFactory.createAccessTokenExampleWithExpireIn(900) + ) + ) + ); + } } -- GitLab