diff --git a/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/RestClient.java b/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/RestClient.java index f2a214a..7876873 100644 --- a/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/RestClient.java +++ b/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/RestClient.java @@ -146,8 +146,8 @@ private T withRetry(String url, @Deprecated private static class ProxyErrorExceptionPropagator implements RestClientExceptionPropagator { @Override - public void propagate(Exception exception, String failedUrl) { - throw ProxyRethrowException.buildProxyException(exception, failedUrl); + public void propagate(Throwable throwable, String failedUrl) { + throw ProxyRethrowException.buildProxyException(throwable, failedUrl); } } } diff --git a/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/ProxyErrorException.java b/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/ProxyErrorException.java index 151b0a0..3e2a0b9 100644 --- a/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/ProxyErrorException.java +++ b/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/ProxyErrorException.java @@ -41,7 +41,7 @@ public ProxyErrorException(ProxyErrorException cause) { this.httpStatus = cause.httpStatus; } - public ProxyErrorException(Exception cause, String url) { + public ProxyErrorException(Throwable cause, String url) { super("Exception while communicating with " + url, cause); ErrorsDescription errorsDescription; this.httpStatus = HttpStatus.EXPECTATION_FAILED; diff --git a/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/ProxyRethrowException.java b/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/ProxyRethrowException.java index 16958dc..0bc9296 100644 --- a/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/ProxyRethrowException.java +++ b/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/ProxyRethrowException.java @@ -25,7 +25,7 @@ public ProxyRethrowException(ProxyErrorException cause) { * @deprecated Change to throw RestClientException when {@link ProxyErrorException} will be removed */ @Deprecated - public static ProxyErrorException buildProxyException(Exception cause, String url) { + public static ProxyErrorException buildProxyException(Throwable cause, String url) { ProxyErrorException e = new ProxyErrorException(cause, url); final ResponseEntity responseEntity = e.getResponseEntity(); if (responseEntity == null || !responseEntity.hasBody()) { diff --git a/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/RestClientExceptionPropagator.java b/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/RestClientExceptionPropagator.java index 3f57654..07b21a2 100644 --- a/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/RestClientExceptionPropagator.java +++ b/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/RestClientExceptionPropagator.java @@ -1,5 +1,5 @@ package com.netcracker.cloud.restlegacy.restclient.error; public interface RestClientExceptionPropagator { - void propagate(Exception exception, String failedUrl); + void propagate(Throwable throwable, String failedUrl); } diff --git a/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/v2/RestClientExceptionPropagatorV2.java b/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/v2/RestClientExceptionPropagatorV2.java index 726fe0c..bdc032e 100644 --- a/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/v2/RestClientExceptionPropagatorV2.java +++ b/restlegacy/restclient/src/main/java/com/netcracker/cloud/restlegacy/restclient/error/v2/RestClientExceptionPropagatorV2.java @@ -7,20 +7,20 @@ class RestClientExceptionPropagatorV2 implements RestClientExceptionPropagator { @Override - public void propagate(Exception exception, String failedUrl) { - propagateIfPossible(exception, RestClientException.class); - throw new RestClientException("Exception while communication with service " + failedUrl, exception); + public void propagate(Throwable throwable, String failedUrl) { + propagateIfPossible(throwable, RestClientException.class); + throw new RestClientException("Exception while communication with service " + failedUrl, throwable); } private void propagateIfPossible (Throwable throwable, Class clazz) throws X { if (clazz.isInstance(throwable)) { throw clazz.cast(throwable); } - if (throwable instanceof RuntimeException) { - throw (RuntimeException) throwable; + if (throwable instanceof RuntimeException runtimeException) { + throw runtimeException; } - if (throwable instanceof Error) { - throw (Error) throwable; + if (throwable instanceof Error error) { + throw error; } } } diff --git a/restlegacy/restclient/src/test/java/com/netcracker/cloud/restlegacy/restclient/error/v2/RestClientExceptionPropagatorV2Test.java b/restlegacy/restclient/src/test/java/com/netcracker/cloud/restlegacy/restclient/error/v2/RestClientExceptionPropagatorV2Test.java new file mode 100644 index 0000000..d8ccd33 --- /dev/null +++ b/restlegacy/restclient/src/test/java/com/netcracker/cloud/restlegacy/restclient/error/v2/RestClientExceptionPropagatorV2Test.java @@ -0,0 +1,61 @@ +package com.netcracker.cloud.restlegacy.restclient.error.v2; + +import org.junit.jupiter.api.Test; +import org.springframework.web.client.RestClientException; + +import static org.junit.jupiter.api.Assertions.*; + +class RestClientExceptionPropagatorV2Test { + + private final RestClientExceptionPropagatorV2 propagator = new RestClientExceptionPropagatorV2(); + private final String failedUrl = "http://test-service"; + + @Test + void propagate_shouldRethrowRestClientExceptionAsIs() { + RestClientException ex = new RestClientException("Already RestClientException"); + + RestClientException thrown = assertThrows( + RestClientException.class, + () -> propagator.propagate(ex, failedUrl) + ); + + assertSame(ex, thrown, "RestClientException should be propagated as-is"); + } + + @Test + void propagate_shouldRethrowRuntimeExceptionAsIs() { + RuntimeException ex = new RuntimeException("Runtime exception"); + + RuntimeException thrown = assertThrows( + RuntimeException.class, + () -> propagator.propagate(ex, failedUrl) + ); + + assertSame(ex, thrown, "RuntimeException should be propagated as-is"); + } + + @Test + void propagate_shouldRethrowErrorAsIs() { + Error error = new OutOfMemoryError("Critical error"); + + Error thrown = assertThrows( + OutOfMemoryError.class, + () -> propagator.propagate(error, failedUrl) + ); + + assertSame(error, thrown, "Error should be propagated as-is"); + } + + @Test + void propagate_shouldWrapCheckedException() { + Exception ex = new Exception("Checked exception"); + + RestClientException thrown = assertThrows( + RestClientException.class, + () -> propagator.propagate(ex, failedUrl) + ); + + assertEquals("Exception while communication with service " + failedUrl, thrown.getMessage()); + assertSame(ex, thrown.getCause(), "Original exception should be wrapped as cause"); + } +} diff --git a/restlegacy/restclient/src/test/java/com/netcracker/cloud/restlegacy/restclient/retry/RetryTemplateBuilderTest.java b/restlegacy/restclient/src/test/java/com/netcracker/cloud/restlegacy/restclient/retry/RetryTemplateBuilderTest.java index 30bd625..6f806f7 100644 --- a/restlegacy/restclient/src/test/java/com/netcracker/cloud/restlegacy/restclient/retry/RetryTemplateBuilderTest.java +++ b/restlegacy/restclient/src/test/java/com/netcracker/cloud/restlegacy/restclient/retry/RetryTemplateBuilderTest.java @@ -1,6 +1,5 @@ package com.netcracker.cloud.restlegacy.restclient.retry; - import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/route-registration/route-registration-common-spring/pom.xml b/route-registration/route-registration-common-spring/pom.xml index 23cd3f3..a115a2f 100644 --- a/route-registration/route-registration-common-spring/pom.xml +++ b/route-registration/route-registration-common-spring/pom.xml @@ -103,6 +103,7 @@ com.squareup.okhttp3 mockwebserver + test diff --git a/route-registration/route-registration-common/src/main/java/com/netcracker/cloud/routesregistration/common/gateway/route/GatewayNameKey.java b/route-registration/route-registration-common/src/main/java/com/netcracker/cloud/routesregistration/common/gateway/route/GatewayNameKey.java index f00730e..ee4efb3 100644 --- a/route-registration/route-registration-common/src/main/java/com/netcracker/cloud/routesregistration/common/gateway/route/GatewayNameKey.java +++ b/route-registration/route-registration-common/src/main/java/com/netcracker/cloud/routesregistration/common/gateway/route/GatewayNameKey.java @@ -13,28 +13,20 @@ public static GatewayNameKey fromGatewayName(String gateway) { // default route type is INTERNAL return INTERNAL; } - if (PUBLIC_GATEWAY_SERVICE.equals(gateway)) { - return PUBLIC; - } - if (PRIVATE_GATEWAY_SERVICE.equals(gateway)) { - return PRIVATE; - } - if (INTERNAL_GATEWAY_SERVICE.equals(gateway)) { - return INTERNAL; - } - return FACADE; + return switch (gateway) { + case PUBLIC_GATEWAY_SERVICE -> PUBLIC; + case PRIVATE_GATEWAY_SERVICE -> PRIVATE; + case INTERNAL_GATEWAY_SERVICE -> INTERNAL; + default -> FACADE; + }; } public String toGatewayName() { - switch (this) { - case PUBLIC: - return PUBLIC_GATEWAY_SERVICE; - case PRIVATE: - return PRIVATE_GATEWAY_SERVICE; - case INTERNAL: - return INTERNAL_GATEWAY_SERVICE; - default: - return null; - } + return switch (this) { + case PUBLIC -> PUBLIC_GATEWAY_SERVICE; + case PRIVATE -> PRIVATE_GATEWAY_SERVICE; + case INTERNAL -> INTERNAL_GATEWAY_SERVICE; + default -> null; + }; } } diff --git a/route-registration/route-registration-common/src/main/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteConfig.java b/route-registration/route-registration-common/src/main/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteConfig.java index e053557..e9488d3 100644 --- a/route-registration/route-registration-common/src/main/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteConfig.java +++ b/route-registration/route-registration-common/src/main/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteConfig.java @@ -2,7 +2,9 @@ import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -44,7 +46,7 @@ public boolean equals(Object o) { return true; } return routes != null && that.routes != null && routes.size() == that.routes.size() - && routes.containsAll(that.routes) && that.routes.containsAll(routes); + && new HashSet<>(routes).containsAll(that.routes) && new HashSet<>(that.routes).containsAll(routes); } @Override @@ -52,3 +54,4 @@ public int hashCode() { return Objects.hash(version); } } + diff --git a/route-registration/route-registration-common/src/main/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteV3.java b/route-registration/route-registration-common/src/main/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteV3.java index d0c7f82..df12684 100644 --- a/route-registration/route-registration-common/src/main/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteV3.java +++ b/route-registration/route-registration-common/src/main/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteV3.java @@ -2,9 +2,13 @@ import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Data @Builder @@ -21,11 +25,12 @@ public RouteV3 merge(RouteV3 anotherRoute) { } private void mergeRules(List rulesToMerge) { - rulesToMerge.forEach(ruleToMerge -> { - if (rules.stream().noneMatch(ruleToMerge::equals)) { - rules.add(ruleToMerge); - } - }); + if (rulesToMerge == null || rulesToMerge.isEmpty()) { + return; + } + + List currentRules = (rules == null) ? List.of() : rules; + this.rules = Stream.concat(currentRules.stream(), rulesToMerge.stream()).distinct().toList(); } @Override @@ -37,7 +42,7 @@ public boolean equals(Object o) { return false; } return rules != null && routeV3.rules != null && rules.size() == routeV3.rules.size() - && rules.containsAll(routeV3.rules) && routeV3.rules.containsAll(rules); + && new HashSet<>(rules).containsAll(routeV3.rules) && new HashSet<>(routeV3.rules).containsAll(rules); } @Override @@ -45,3 +50,4 @@ public int hashCode() { return Objects.hash(destination); } } + diff --git a/route-registration/route-registration-common/src/test/java/com/netcracker/cloud/routesregistration/common/gateway/route/GatewayNameKeyTest.java b/route-registration/route-registration-common/src/test/java/com/netcracker/cloud/routesregistration/common/gateway/route/GatewayNameKeyTest.java new file mode 100644 index 0000000..62e46f4 --- /dev/null +++ b/route-registration/route-registration-common/src/test/java/com/netcracker/cloud/routesregistration/common/gateway/route/GatewayNameKeyTest.java @@ -0,0 +1,25 @@ +package com.netcracker.cloud.routesregistration.common.gateway.route; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class GatewayNameKeyTest { + @Test + void fromGatewayName() { + assertEquals(GatewayNameKey.PUBLIC, GatewayNameKey.fromGatewayName("public-gateway-service")); + assertEquals(GatewayNameKey.PRIVATE, GatewayNameKey.fromGatewayName("private-gateway-service")); + assertEquals(GatewayNameKey.INTERNAL, GatewayNameKey.fromGatewayName("internal-gateway-service")); + assertEquals(GatewayNameKey.FACADE, GatewayNameKey.fromGatewayName("some-facade-gateway-service")); + assertEquals(GatewayNameKey.INTERNAL, GatewayNameKey.fromGatewayName(null)); + assertEquals(GatewayNameKey.INTERNAL, GatewayNameKey.fromGatewayName("")); + } + + @Test + void toGatewayName() { + assertEquals("public-gateway-service", GatewayNameKey.PUBLIC.toGatewayName()); + assertEquals("private-gateway-service", GatewayNameKey.PRIVATE.toGatewayName()); + assertEquals("internal-gateway-service", GatewayNameKey.INTERNAL.toGatewayName()); + assertNull(GatewayNameKey.FACADE.toGatewayName()); + } +} diff --git a/route-registration/route-registration-common/src/test/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteConfigTest.java b/route-registration/route-registration-common/src/test/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteConfigTest.java new file mode 100644 index 0000000..004c846 --- /dev/null +++ b/route-registration/route-registration-common/src/test/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteConfigTest.java @@ -0,0 +1,117 @@ +package com.netcracker.cloud.routesregistration.common.gateway.route.v3.domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class RouteV3Test { + + private RouteDestination destination; + private RouteV3 route1; + private RouteV3 route2; + private Rule ruleA; + private Rule ruleB; + private Rule ruleDuplicate; + + @BeforeEach + void setUp() { + destination = RouteDestination.builder() + .cluster("cluster-1") + .endpoint("endpoint-1") + .build(); + + ruleA = Rule.builder() + .prefixRewrite("/api/v1") + .allowed(true) + .timeout(1000L) + .build(); + + ruleB = Rule.builder() + .prefixRewrite("/api/v2") + .allowed(false) + .timeout(2000L) + .build(); + + ruleDuplicate = Rule.builder() + .prefixRewrite("/api/v1") + .allowed(true) + .timeout(1000L) + .build(); // identical to ruleA + + route1 = RouteV3.builder() + .destination(destination) + .rules(List.of(ruleA)) + .build(); + + route2 = RouteV3.builder() + .destination(destination) + .rules(List.of(ruleB, ruleDuplicate)) + .build(); + } + + @Test + void merge_shouldCombineUniqueRules() { + RouteV3 merged = route1.merge(route2); + + assertEquals(2, route1.getRules().size()); + assertTrue(route1.getRules().contains(ruleA)); + assertTrue(route1.getRules().contains(ruleB)); + assertTrue(route1.getRules().contains(ruleDuplicate)); + assertSame(merged, route1); + } + + @Test + void merge_shouldNotAddDuplicateRules() { + route1.merge(RouteV3.builder() + .destination(destination) + .rules(List.of(ruleDuplicate)) + .build()); + + assertEquals(1, route1.getRules().size(), "Duplicate rule should not be added"); + } + + @Test + void merge_shouldThrowExceptionForDifferentDestinations() { + RouteDestination otherDestination = RouteDestination.builder() + .cluster("cluster-2") + .endpoint("endpoint-2") + .build(); + + RouteV3 routeWithDifferentDest = RouteV3.builder() + .destination(otherDestination) + .rules(List.of(ruleB)) + .build(); + + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> route1.merge(routeWithDifferentDest) + ); + + assertEquals("Cannot merge RouteV3 with different RouteDestination", ex.getMessage()); + } + + @Test + void merge_shouldHandleEmptyRules() { + RouteV3 emptyRulesRoute = RouteV3.builder() + .destination(destination) + .rules(List.of()) + .build(); + + route1.merge(emptyRulesRoute); + assertEquals(1, route1.getRules().size()); + } + + @Test + void merge_shouldHandleNullRuleListsSafely() { + RouteV3 nullRulesRoute = RouteV3.builder() + .destination(destination) + .rules(null) + .build(); + + assertDoesNotThrow(() -> route1.merge(nullRulesRoute)); + assertEquals(1, route1.getRules().size()); + } +} diff --git a/route-registration/route-registration-common/src/test/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteConfigurationRequestV3Test.java b/route-registration/route-registration-common/src/test/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteConfigurationRequestV3Test.java new file mode 100644 index 0000000..6975009 --- /dev/null +++ b/route-registration/route-registration-common/src/test/java/com/netcracker/cloud/routesregistration/common/gateway/route/v3/domain/RouteConfigurationRequestV3Test.java @@ -0,0 +1,101 @@ +package com.netcracker.cloud.routesregistration.common.gateway.route.v3.domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class RouteConfigurationRequestV3Test { + + private RouteConfigurationRequestV3 request1; + private RouteConfigurationRequestV3 request2; + private VirtualService vs1; + private VirtualService vs2; + private VirtualService vs3; + + @BeforeEach + void setUp() { + // Mock RouteConfig + RouteConfig routeConfig1 = mock(RouteConfig.class); + RouteConfig routeConfig2 = mock(RouteConfig.class); + RouteConfig routeConfig3 = mock(RouteConfig.class); + + // Create virtual services with non-null RouteConfig + vs1 = spy(VirtualService.builder().name("service1").routeConfiguration(routeConfig1).build()); + vs2 = spy(VirtualService.builder().name("service2").routeConfiguration(routeConfig2).build()); + vs3 = spy(VirtualService.builder().name("service3").routeConfiguration(routeConfig3).build()); + + // Request 1 has vs1 and vs2 + request1 = RouteConfigurationRequestV3.builder() + .namespace("default") + .gateways(new ArrayList<>(List.of("gateway1"))) + .virtualServices(new ArrayList<>(List.of(vs1, vs2))) + .build(); + + // Request 2 has vs2 (overlap) and vs3 (new) + request2 = RouteConfigurationRequestV3.builder() + .namespace("default") + .gateways(new ArrayList<>(List.of("gateway1"))) + .virtualServices(new ArrayList<>(List.of(vs2, vs3))) + .build(); + } + + @Test + void merge_shouldAddNewVirtualServices() { + request1.merge(request2); + + assertEquals(3, request1.getVirtualServices().size(), "Should have 3 virtual services after merge"); + assertTrue(request1.getVirtualServices().stream().anyMatch(v -> v.getName().equals("service3")), + "New virtual service vs3 should be added"); + } + + @Test + void merge_shouldCallMergeOnOverlappingVirtualServices() { + request1.merge(request2); + + // vs2 exists in both, merge should be called + verify(vs2, times(1)).merge(any(VirtualService.class)); + + // vs1 and vs3 are not overlapping, merge should not be called on them + verify(vs1, never()).merge(any(VirtualService.class)); + verify(vs3, never()).merge(any(VirtualService.class)); + } + + @Test + void merge_shouldThrowIfGatewaysDoNotMatch() { + RouteConfigurationRequestV3 other = RouteConfigurationRequestV3.builder() + .namespace("default") + .gateways(List.of("differentGateway")) + .virtualServices(List.of(vs3)) + .build(); + + IllegalStateException ex = assertThrows(IllegalStateException.class, + () -> request1.merge(other)); + + assertTrue(ex.getMessage().contains("Both RouteConfigurationRequestV3 must have exactly one same 'gateways'")); + } + + @Test + void merge_shouldThrowIfMultipleGateways() { + RouteConfigurationRequestV3 other = RouteConfigurationRequestV3.builder() + .namespace("default") + .gateways(List.of("gateway1", "gateway2")) + .virtualServices(List.of(vs3)) + .build(); + + IllegalStateException ex = assertThrows(IllegalStateException.class, + () -> request1.merge(other)); + + assertTrue(ex.getMessage().contains("Both RouteConfigurationRequestV3 must have exactly one same 'gateways'")); + } + + @Test + void merge_shouldReturnThis() { + RouteConfigurationRequestV3 result = request1.merge(request2); + assertSame(request1, result, "merge should return the original request1 instance"); + } +} diff --git a/security/pom.xml b/security/pom.xml index 4cc99fb..10e9bf7 100644 --- a/security/pom.xml +++ b/security/pom.xml @@ -51,5 +51,22 @@ 3.7.11 true + + org.junit.jupiter + junit-jupiter + 5.13.4 + test + + + org.mockito + mockito-core + 5.19.0 + test + + + org.assertj + assertj-core + 3.27.4 + diff --git a/security/src/test/java/com/netcracker/cloud/security/common/webclient/DefaultSmartWebClientTest.java b/security/src/test/java/com/netcracker/cloud/security/common/webclient/DefaultSmartWebClientTest.java new file mode 100644 index 0000000..17924a3 --- /dev/null +++ b/security/src/test/java/com/netcracker/cloud/security/common/webclient/DefaultSmartWebClientTest.java @@ -0,0 +1,138 @@ +package com.netcracker.cloud.security.common.webclient; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.List; +import java.util.function.Consumer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class DefaultSmartWebClientTest { + + private WebClient.Builder builder; + private WebClient.Builder clonedBuilder; + private WebClient webClient; + + private ExchangeFilterFunction genericInterceptor1; + private ExchangeFilterFunction genericInterceptor2; + private ExchangeFilterFunction m2mAuth; + private ExchangeFilterFunction userAuth; + private ExchangeFilterFunction securityContextInterceptor; + private AuthorizationHeaderInterceptorFactory factory; + + private DefaultSmartWebClient client; + + @BeforeEach + void setUp() { + builder = mock(WebClient.Builder.class); + clonedBuilder = mock(WebClient.Builder.class); + webClient = mock(WebClient.class); + + when(builder.clone()).thenReturn(clonedBuilder); + when(clonedBuilder.build()).thenReturn(webClient); + when(clonedBuilder.filter(any())).thenReturn(clonedBuilder); + when(clonedBuilder.filters(any())).thenReturn(clonedBuilder); + + genericInterceptor1 = mock(ExchangeFilterFunction.class); + genericInterceptor2 = mock(ExchangeFilterFunction.class); + + m2mAuth = mock(ExchangeFilterFunction.class); + userAuth = mock(ExchangeFilterFunction.class); + securityContextInterceptor = mock(ExchangeFilterFunction.class); + + factory = mock(AuthorizationHeaderInterceptorFactory.class); + when(factory.build(Mode.M2M)).thenReturn(m2mAuth); + when(factory.build(Mode.USER)).thenReturn(userAuth); + + client = new DefaultSmartWebClient( + List.of(genericInterceptor1, genericInterceptor2), + securityContextInterceptor, + factory, + builder + ); + } + + @Test + void getWebClientBuilderForMode_DEFAULT_withSecurityContext_shouldIncludeInterceptor() { + client.getWebClientBuilderForMode(Mode.DEFAULT); + + verify(builder).clone(); + verify(clonedBuilder).filters(any()); + verify(clonedBuilder).filter(securityContextInterceptor); + } + + @Test + void getWebClientBuilderForMode_DEFAULT_withoutSecurityContext_shouldNotIncludeInterceptor() { + client = new DefaultSmartWebClient( + List.of(genericInterceptor1), + null, + factory, + builder + ); + + client.getWebClientBuilderForMode(Mode.DEFAULT); + + verify(builder).clone(); + verify(clonedBuilder).filters(any()); + verify(clonedBuilder, never()).filter(any()); + } + + @Test + void getWebClientBuilderForMode_M2M_shouldApplyM2mAuthHeader() { + client.getWebClientBuilderForMode(Mode.M2M); + + verify(clonedBuilder).filter(m2mAuth); + verify(factory).build(Mode.M2M); + } + + @Test + void getWebClientBuilderForMode_USER_shouldApplyUserAuthHeader() { + client.getWebClientBuilderForMode(Mode.USER); + + verify(clonedBuilder).filter(userAuth); + verify(factory).build(Mode.USER); + } + + @Test + void getWebClientBuilder_shouldDelegateToDefaultMode() { + client.getWebClientBuilder(); + + verify(builder).clone(); + verify(clonedBuilder).filters(any()); + } + + @Test + void getWebClientForM2mAuthorization_shouldBuildWebClient() { + client.getWebClientForM2mAuthorization(); + + verify(clonedBuilder).filter(m2mAuth); + verify(clonedBuilder).build(); + } + + @Test + void getWebClientForUserAuthorization_shouldBuildWebClient() { + client.getWebClientForUserAuthorization(); + + verify(clonedBuilder).filter(userAuth); + verify(clonedBuilder).build(); + } + + @Test + void prepareWebClient_shouldApplyAllGenericInterceptors() { + ArgumentCaptor>> captor = ArgumentCaptor.forClass(java.util.function.Consumer.class); + + client.getWebClientBuilderForMode(Mode.M2M); + + verify(clonedBuilder).filters(captor.capture()); + + List filtersList = mock(List.class); + captor.getValue().accept(filtersList); + + verify(filtersList, times(2)).add(any()); + } +} diff --git a/webclient/pom.xml b/webclient/pom.xml index de36c26..6b4e244 100644 --- a/webclient/pom.xml +++ b/webclient/pom.xml @@ -89,6 +89,16 @@ 2.0.17 test + + org.springframework.boot + spring-boot-test + test + + + jakarta.servlet + jakarta.servlet-api + test + diff --git a/webclient/src/test/java/com/netcracker/cloud/smartclient/rest/webclient/sample/ApplicationTest.java b/webclient/src/test/java/com/netcracker/cloud/smartclient/rest/webclient/sample/ApplicationTest.java index bc66dbf..112a25e 100644 --- a/webclient/src/test/java/com/netcracker/cloud/smartclient/rest/webclient/sample/ApplicationTest.java +++ b/webclient/src/test/java/com/netcracker/cloud/smartclient/rest/webclient/sample/ApplicationTest.java @@ -1,120 +1,121 @@ -//package com.netcracker.cloud.smartclient.rest.webclient.sample; -// -//import com.netcracker.cloud.context.propagation.core.ContextManager; -//import com.netcracker.cloud.context.propagation.spring.webclient.interceptor.SpringWebClientInterceptor; -//import com.sun.net.httpserver.HttpServer; -//import org.junit.AfterClass; -//import org.junit.Assert; -//import org.junit.BeforeClass; -//import org.junit.Test; -//import org.junit.runner.RunWith; -//import com.netcracker.cloud.framework.contexts.tenant.TenantContextObject; -//import com.netcracker.cloud.security.common.webclient.SmartWebClient; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.http.HttpStatus; -//import org.springframework.http.ResponseEntity; -//import org.springframework.test.context.ContextConfiguration; -//import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -//import org.springframework.web.reactive.function.client.ExchangeFilterFunction; -//import org.springframework.web.reactive.function.client.WebClient; -// -//import java.lang.reflect.Field; -//import java.net.HttpURLConnection; -//import java.net.InetSocketAddress; -//import java.util.List; -//import java.util.Objects; -// -//import static com.netcracker.cloud.framework.contexts.tenant.TenantProvider.TENANT_CONTEXT_NAME; -// -//@RunWith(SpringJUnit4ClassRunner.class) -//@ContextConfiguration(classes = {TestConfig.class}) -//public class ApplicationTest { -// -// -// @Autowired -// SmartWebClient smartWebClient; -// @Autowired -// SpringWebClientInterceptor springWebClientInterceptor; -// -// private static HttpServer httpServer; -// -// @BeforeClass -// public static void initServer() throws Exception { -// ContextManager.set(TENANT_CONTEXT_NAME, new TenantContextObject("")); -// httpServer = HttpServer.create(new InetSocketAddress(8181), 0); -// -// httpServer.createContext("/test", exchange -> { -// byte[] response; -// int responseCode; -// switch (exchange.getRequestMethod()) { -// case "OPTIONS": -// response = null; -// responseCode = HttpURLConnection.HTTP_OK; -// exchange.getResponseHeaders().add("Allow", "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD"); -// break; -// case "HEAD": -// response = null; -// responseCode = HttpURLConnection.HTTP_NO_CONTENT; -// exchange.getResponseHeaders().add("test-header-name", "test-header-value"); -// break; -// default: -// response = "{\"success\": true}".getBytes(); -// responseCode = HttpURLConnection.HTTP_OK; -// break; -// } -// exchange.sendResponseHeaders(responseCode, response == null ? 0 : response.length); -// if (response != null) { -// exchange.getResponseBody().write(response); -// } -// exchange.close(); -// }); -// httpServer.start(); -// } -// -// @AfterClass -// public static void stopHttpServer() { -// httpServer.stop(0); -// } -// -// @Autowired -// private WebClient userWebClient; -// -// @Test -// public void getWebClient() { -// Assert.assertEquals(HttpStatus.OK, Objects.requireNonNull(userWebClient.get().uri("http://localhost:8181/test").retrieve().toBodilessEntity().block()).getStatusCode()); -// } -// -// @Test -// public void postWebClient() { -// Assert.assertEquals(HttpStatus.OK, Objects.requireNonNull(userWebClient.post().uri("http://localhost:8181/test").retrieve().toBodilessEntity().block()).getStatusCode()); -// } -// -// @Test -// public void patchWebClient() { -// ResponseEntity response = userWebClient.patch().uri("http://localhost:8181/test").retrieve().toBodilessEntity().block(); -// Assert.assertEquals(HttpStatus.OK, response.getStatusCode()); -// } -// -// @Test -// public void deleteWebClient() { -// Assert.assertEquals(HttpStatus.OK, Objects.requireNonNull(userWebClient.delete().uri("http://localhost:8181/test").retrieve().toBodilessEntity().block()).getStatusCode()); -// } -// -// @Test -// public void optionWebClient() { -// Assert.assertEquals(HttpStatus.OK, Objects.requireNonNull(userWebClient.options().uri("http://localhost:8181/test").retrieve().toBodilessEntity().block()).getStatusCode()); -// } -// -// @Test -// public void headWebClient() { -// Assert.assertEquals(HttpStatus.NO_CONTENT, Objects.requireNonNull(userWebClient.head().uri("http://localhost:8181/test").retrieve().toBodilessEntity().block()).getStatusCode()); -// } -// -// @Test -// public void smartWebClientInterceptorsTest() throws NoSuchFieldException, IllegalAccessException { -// Field genericInterceptors = smartWebClient.getClass().getDeclaredField("genericInterceptors"); -// genericInterceptors.setAccessible(true); -// List interceptors = (List) genericInterceptors.get(smartWebClient); -// Assert.assertTrue(interceptors.contains(springWebClientInterceptor)); -// } -//} +package com.netcracker.cloud.smartclient.rest.webclient.sample; + +import com.netcracker.cloud.context.propagation.core.ContextManager; +import com.netcracker.cloud.context.propagation.spring.webclient.interceptor.SpringWebClientInterceptor; +import com.netcracker.cloud.framework.contexts.tenant.TenantContextObject; +import com.netcracker.cloud.security.common.webclient.SmartWebClient; +import com.sun.net.httpserver.HttpServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +import java.lang.reflect.Field; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Objects; + +import static com.netcracker.cloud.framework.contexts.tenant.BaseTenantProvider.TENANT_CONTEXT_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = {TestConfig.class}) +class ApplicationTest { + + @Autowired + SmartWebClient smartWebClient; + + @Autowired + SpringWebClientInterceptor springWebClientInterceptor; + + private static HttpServer httpServer; + + @BeforeAll + static void initServer() throws Exception { + ContextManager.set(TENANT_CONTEXT_NAME, new TenantContextObject("")); + httpServer = HttpServer.create(new InetSocketAddress(8181), 0); + + httpServer.createContext("/test", exchange -> { + byte[] response; + int responseCode; + switch (exchange.getRequestMethod()) { + case "OPTIONS": + response = null; + responseCode = HttpURLConnection.HTTP_OK; + exchange.getResponseHeaders().add("Allow", "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD"); + break; + case "HEAD": + response = null; + responseCode = HttpURLConnection.HTTP_NO_CONTENT; + exchange.getResponseHeaders().add("test-header-name", "test-header-value"); + break; + default: + response = "{\"success\": true}".getBytes(); + responseCode = HttpURLConnection.HTTP_OK; + break; + } + exchange.sendResponseHeaders(responseCode, response == null ? 0 : response.length); + if (response != null) { + exchange.getResponseBody().write(response); + } + exchange.close(); + }); + httpServer.start(); + } + + @AfterAll + static void stopHttpServer() { + httpServer.stop(0); + } + + @Autowired + private WebClient userWebClient; + + @Test + void getWebClient() { + assertEquals(HttpStatus.OK, Objects.requireNonNull(userWebClient.get().uri("http://localhost:8181/test").retrieve().toBodilessEntity().block()).getStatusCode()); + } + + @Test + void postWebClient() { + assertEquals(HttpStatus.OK, Objects.requireNonNull(userWebClient.post().uri("http://localhost:8181/test").retrieve().toBodilessEntity().block()).getStatusCode()); + } + + @Test + void patchWebClient() { + ResponseEntity response = userWebClient.patch().uri("http://localhost:8181/test").retrieve().toBodilessEntity().block(); + assertEquals(HttpStatus.OK, response.getStatusCode()); + } + + @Test + void deleteWebClient() { + assertEquals(HttpStatus.OK, Objects.requireNonNull(userWebClient.delete().uri("http://localhost:8181/test").retrieve().toBodilessEntity().block()).getStatusCode()); + } + + @Test + void optionWebClient() { + assertEquals(HttpStatus.OK, Objects.requireNonNull(userWebClient.options().uri("http://localhost:8181/test").retrieve().toBodilessEntity().block()).getStatusCode()); + } + + @Test + void headWebClient() { + assertEquals(HttpStatus.NO_CONTENT, Objects.requireNonNull(userWebClient.head().uri("http://localhost:8181/test").retrieve().toBodilessEntity().block()).getStatusCode()); + } + + @Test + void smartWebClientInterceptorsTest() throws NoSuchFieldException, IllegalAccessException { + Field genericInterceptors = smartWebClient.getClass().getDeclaredField("genericInterceptors"); + genericInterceptors.setAccessible(true); + List interceptors = (List) genericInterceptors.get(smartWebClient); + assertTrue(interceptors.contains(springWebClientInterceptor)); + } +} diff --git a/webclient/src/test/java/com/netcracker/cloud/smartclient/rest/webclient/sample/TestConfig.java b/webclient/src/test/java/com/netcracker/cloud/smartclient/rest/webclient/sample/TestConfig.java index da86a6f..c18e499 100644 --- a/webclient/src/test/java/com/netcracker/cloud/smartclient/rest/webclient/sample/TestConfig.java +++ b/webclient/src/test/java/com/netcracker/cloud/smartclient/rest/webclient/sample/TestConfig.java @@ -7,6 +7,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.web.reactive.function.client.WebClient; import java.util.Properties; @@ -14,6 +15,12 @@ @EnableFrameworkWebClient public class TestConfig { + @Bean + @ConditionalOnMissingBean + public WebClient.Builder webClientBuilder() { + return WebClient.builder(); + } + @Bean @ConditionalOnMissingBean public RestTemplateBuilder restTemplateBuilder() {