I'm trying to mock the following method:
public Mono<PResponse> pay(final String oId,final Double amount) {
return webClient
.put()
.uri("/order/{oId}/amount/{amount}",oId,amount)
.body(BodyInserts
.fromObject(PRequest))
.exchange()
.flatMap(
response -> {
if(response.statusCode().is4xxClientError()) {
// call error Function
} else {
return response
.bodyToMono(PResponse.class)
.flatMap(pResponse -> {
return Mono.just(pResposne)
});
}
}
);
}
For your information, webClient is a private Instance.
You can use MockWebServer.Here is an example, using code from this blog post:
Service
class ApiCaller {
private WebClient webClient;
ApiCaller(WebClient webClient) {
this.webClient = webClient;
}
Mono<SimpleResponseDto> callApi() {
return webClient.put()
.uri("/api/resource")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "customAuth")
.syncBody(new SimpleRequestDto())
.retrieve()
.bodyToMono(SimpleResponseDto.class);
}
}
Test
class ApiCallerTest {
private final MockWebServer mockWebServer = new MockWebServer();
private final ApiCaller apiCaller = new ApiCaller(WebClient.create(mockWebServer.url("/").toString()));
#AfterEach
void tearDown() throws IOException {
mockWebServer.shutdown();
}
#Test
void call() throws InterruptedException {
mockWebServer.enqueue(
new MockResponse()
.setResponseCode(200)
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.setBody("{\"y\": \"value for y\", \"z\": 789}")
);
SimpleResponseDto response = apiCaller.callApi().block();
assertThat(response, is(not(nullValue())));
assertThat(response.getY(), is("value for y"));
assertThat(response.getZ(), is(789));
RecordedRequest recordedRequest = mockWebServer.takeRequest();
//use method provided by MockWebServer to assert the request header
recordedRequest.getHeader("Authorization").equals("customAuth");
DocumentContext context = JsonPath.parse(recordedRequest.getBody().inputStream());
//use JsonPath library to assert the request body
assertThat(context, isJson(allOf(
withJsonPath("$.a", is("value1")),
withJsonPath("$.b", is(123))
)));
}
}
Related
I need to call an third party API which requires an authentication call beforehand to get an authentication token. The Authentication API is in json but the subsequent call is in XML.
I have separately :
webclient.post().uri("/auth").header(ACCEPT,JSON).retrieve()
.bodyToMono(AuthToken.class);
webclient.post().uri("/api").header(ACCEPT,XML).header("AUTH",authToken).retrive().bodyToFlux();
How should I implement the method to be able to access the second API?
I tried to assign a variable inside the method with token = firstCall.block() but I've got block() is not supported error.
You just have to transform the original flux like:
webclient.post().uri("/auth")
.header(ACCEPT,JSON)
.retrieve()
.bodyToMono(AuthToken.class)
.flatMapMany(authToken -> webclient.post().uri("/api")
.header(ACCEPT,XML)
.header("AUTH",authToken).retrive().bodyToFlux();
A better solution would be to use a ExchangeFilterFunction that will fetch the token for you https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web-reactive.html#webflux-client-filter
Something like that (not tested might have bug):
WebClient authWebClient = WebClient.builder().build();
WebClient webClient = WebClient.builder()
.filter(((request, next) -> authWebClient.post()
.uri("/auth")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(AuthToken.class)
.flatMap(authToken -> next.exchange(ClientRequest.from(request)
.headers(headers -> headers.add("AUTH", authToken))
.build()))
))
.build();
webClient.post().uri("/api")
.accept(MediaType.APPLICATION_XML)
.retrieve()
.bodyToFlux(MyData.class);
This is basic but you could add cache to avoid requesting or fetch again if token is expired...
Be aware that builtin ExchangeFilterFunction exists for basic oauth2...
Wrap everything with a spring configuration:
#Configuration
public class WebClientConfiguration {
#Bean
public WebClient authWebClient(final WebClient.Builder webClientBuilder) {
return webClientBuilder.build();
}
#Bean
public ExchangeFilterFunction authFilter(final WebClient authWebClient) {
return (request, next) -> authWebClient.post()
.uri("/auth")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(AuthToken.class)
.flatMap(authToken -> next.exchange(ClientRequest.from(request)
.headers(headers -> headers.add("AUTH", authToken.toString()))
.build()));
}
#Bean
public WebClient webClient(final WebClient.Builder webClientBuilder, final ExchangeFilterFunction authFilter) {
return webClientBuilder
.filter(authFilter)
.build();
}
}
Or if you want to avoid lambda:
#Configuration
public class WebClientConfiguration {
#Bean
public WebClient authWebClient(final WebClient.Builder webClientBuilder) {
return webClientBuilder.build();
}
#Bean
public WebClient webClient(final WebClient.Builder webClientBuilder, final AuthFilter authFilter) {
return webClientBuilder
.filter(authFilter)
.build();
}
#Bean
public AuthFilter authFilter(WebClient authWebClient) {
return new AuthFilter(authWebClient);
}
}
public class AuthFilter implements ExchangeFilterFunction {
private final WebClient authWebClient;
public AuthFilter(WebClient authWebClient) {
this.authWebClient = authWebClient;
}
#Override
public Mono<ClientResponse> filter(final ClientRequest request, final ExchangeFunction next) {
return authWebClient.post()
.uri("/auth")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(AuthToken.class)
.flatMap(authToken -> next.exchange(ClientRequest.from(request)
.headers(headers -> headers.add("AUTH", authToken.toString()))
.build()));
}
}
io.netty.channel.unix.Errors$NativeIoException: newSocketStream(..) failed: Too many open files
I am searching for a long time on the net. But no use. Please help or try to give some ideas on how to achieve this.
spring-boot: 2.3.1.RELEASE
#Configuration
public class AppConfig {
#Bean
public WebClient webClient(WebClient.Builder builder) throws SSLException {
final SslContext sslContext =
SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
HttpClient httpClient =
HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContext));
return builder.clientConnector(new ReactorClientHttpConnector(httpClient)).build();
}
#Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
#RestController
#RequestMapping("/test")
public class WebClientTestController {
private final WebClient webClient;
private final RestTemplate restTemplate;
public WebClientTestController(WebClient webClient, RestTemplate restTemplate) {
this.webClient = webClient;
this.restTemplate = restTemplate;
}
#GetMapping("/webclient")
public Mono<Object> main() {
new SimpleAsyncTaskExecutor()
.execute(
() -> {
for (int i = 0; i <= 65536; ++i) {
System.out.println(
webClient
.get()
.uri("http://127.0.0.1:8080/hello")
.retrieve()
.bodyToMono(String.class)
.block()
+ i);
}
});
return Mono.empty();
}
#GetMapping("/restTemplate")
public Mono<Object> main2() {
new SimpleAsyncTaskExecutor()
.execute(
() -> {
for (int i = 0; i <= 65536; ++i) {
System.out.println(
restTemplate.getForEntity("http://127.0.0.1:8080/hello", String.class).getBody()
+ i);
}
});
return Mono.empty();
}
}
#Component
public class GreetingHandler {
public Mono<ServerResponse> hello(ServerRequest request) {
return ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromValue("Hello, Spring!"));
}
}
#Configuration
public class GreetingRouter {
#Bean
public RouterFunction<ServerResponse> route(GreetingHandler greetingHandler) {
return RouterFunctions.route(
RequestPredicates.GET("/hello").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
greetingHandler::hello);
}
}
request in windows:
curl http://127.0.0.1:8080/test/restTemplate
...
Hello, Spring!65535
Hello, Spring!65536
curl http://127.0.0.1:8080/test/webclient
...
Hello, Spring!65535
Hello, Spring!65536
request in Linux:
curl http://127.0.0.1:8080/test/restTemplate
...
Hello, Spring!65535
Hello, Spring!65536
curl http://127.0.0.1:8080/test/webclient
Hello, Spring!10196
Hello, Spring!10197
Exception in thread "SimpleAsyncTaskExecutor-1" io.netty.channel.ChannelException: io.netty.channel.unix.Errors$NativeIoException: newSocketStream(..) failed: Too many open files
at io.netty.channel.unix.Socket.newSocketStream0(Socket.java:421)
at io.netty.channel.epoll.LinuxSocket.newSocketStream(LinuxSocket.java:319)
at io.netty.channel.epoll.LinuxSocket.newSocketStream(LinuxSocket.java:323)
at io.netty.channel.epoll.EpollSocketChannel.<init>(EpollSocketChannel.java:45)
at reactor.netty.resources.DefaultLoopEpoll.getChannel(DefaultLoopEpoll.java:45)
at reactor.netty.resources.LoopResources.onChannel(LoopResources.java:187)
at reactor.netty.resources.LoopResources.onChannel(LoopResources.java:169)
at reactor.netty.tcp.TcpResources.onChannel(TcpResources.java:215)
at reactor.netty.http.client.HttpClientConnect$HttpTcpClient.connect(HttpClientConnect.java:141)
at reactor.netty.tcp.TcpClientOperator.connect(TcpClientOperator.java:43)
at reactor.netty.tcp.TcpClientOperator.connect(TcpClientOperator.java:43)
at reactor.netty.tcp.TcpClientOperator.connect(TcpClientOperator.java:43)
at reactor.netty.tcp.TcpClientOperator.connect(TcpClientOperator.java:43)
at reactor.netty.tcp.TcpClient.connect(TcpClient.java:202)
at reactor.netty.http.client.HttpClientFinalizer.connect(HttpClientFinalizer.java:77)
at reactor.netty.http.client.HttpClientFinalizer.responseConnection(HttpClientFinalizer.java:94)
at org.springframework.http.client.reactive.ReactorClientHttpConnector.connect(ReactorClientHttpConnector.java:111)
at org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction.exchange(ExchangeFunctions.java:104)
at org.springframework.web.reactive.function.client.DefaultWebClient$DefaultRequestBodyUriSpec.lambda$exchange$0(DefaultWebClient.java:338)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44)
at reactor.core.publisher.Mono.subscribe(Mono.java:4219)
at reactor.core.publisher.Mono.block(Mono.java:1678)
at com.example.demo.controller.WebClientTestController.lambda$main$0(WebClientTestController.java:38)
at java.lang.Thread.run(Thread.java:748)
Suppressed: java.lang.Exception: #block terminated with an error
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:99)
at reactor.core.publisher.Mono.block(Mono.java:1679)
... 2 more
Caused by: io.netty.channel.unix.Errors$NativeIoException: newSocketStream(..) failed: Too many open files
≈ ulimit -n
Where is wrong? I hope to learn more details.
I've got a lot of methods that use the onStatus API from Spring's WebClient:
#Override
public Mono<Accommodation> createAccommodation(CreateAccommodation create) {
return webClient
.post()
.contentType(APPLICATION_JSON)
.bodyValue(create)
.retrieve()
.onStatus(HttpStatus::isError,
clientResponse -> clientResponse
.bodyToMono(ApiErrorResponse.class)
.flatMap(errorResponse -> Mono.error(new ResponseStatusException(
HttpStatus.valueOf(errorResponse.getStatus()),
errorResponse.getMessage()
))))
.bodyToMono(Accommodation.class);
}
What I would like to do is to avoid having to use the "onStatus" in every single WebClient call.
Is there a way to set this when building the WebClient instance? Can you show some examples?
This is my WebClient instance:
public AccommodationServiceClientImpl(WebClient.Builder builder) {
this.webClient = builder
.baseUrl("lb://accommodation-service/api/v1/accommodations")
.build();
}
Found a solution: ExchangeFilterFunction.ofResponseProcessor seems to be what I was looking for.
#Configuration
public class WebClientConfig {
#Bean
#LoadBalanced
public WebClient.Builder webClientBuilder(){
return WebClient
.builder()
.filter(ExchangeFilterFunction.ofResponseProcessor(this::renderApiErrorResponse));
}
private Mono<ClientResponse> renderApiErrorResponse(ClientResponse clientResponse) {
if(clientResponse.statusCode().isError()){
return clientResponse.bodyToMono(ApiErrorResponse.class)
.flatMap(apiErrorResponse -> Mono.error(new ResponseStatusException(
clientResponse.statusCode(),
apiErrorResponse.getMessage()
)));
}
return Mono.just(clientResponse);
}
}
I am trying to find a solution which will build a new link using X-Forwarded-* headers.
public class ApiUriBuilderTest {
private MockHttpServletRequest request = new MockHttpServletRequest();
private HttpRequest httpRequest = new ServletServerHttpRequest(request);
#Before
public void setUp() throws Exception {
request.setScheme("http");
request.setServerName("localhost");
request.setServerPort(80);
request.setRequestURI("/mvc-showcase");
request.addHeader("X-Forwarded-Proto", "https");
request.addHeader("X-Forwarded-Host", "84.198.58.199");
request.addHeader("X-Forwarded-Port", "443");
request.setContextPath("/mvc-showcase");
request.setServletPath("/app");
request.setRequestURI("/mvc-showcase/app/uri/of/request?hello=world&raw#my-frag");
httpRequest = new ServletServerHttpRequest(request);
}
#Test
public void test() {
String uri = ForwardedContextPathServletUriComponentsBuilder.fromRequest(request).build().toUriString();
assertThat(uri, is("https://84.198.58.199:443"));
}
#Test
public void test_uri_components_builder() throws URISyntaxException {
UriComponents result = UriComponentsBuilder.fromHttpRequest(httpRequest).build();
assertEquals("https://84.198.58.199:443", result.toString());
}
But the returning value is "https://84.198.58.199/mvc-showcase/app/uri/of/request?hello=world&raw#my-frag". How can I possible get rid of context-path, setvlet-path and request uri?
#Test
public void test() {
String uri = ServletUriComponentsBuilder.fromRequest(request).replacePath("relativePath").replaceQuery(null).build().toUriString();
assertThat(uri, is("https://84.198.58.199:8080/relativePath"));
}
helped.
I want to perform a test on a controller method which throws an exception. The method is something like this:
#RequestMapping("/do")
public ResponseEntity doIt(#RequestBody Request request) throws Exception {
throw new NullPointerException();
}
When I try to test this method with following code part,
mockMvc.perform(post("/do")
.contentType(MediaType.APPLICATION_JSON)
.content(JSON.toJson(request)))
NestedServletException is thrown from Spring libraries. How can I test that NullPointerException is thrown instead of NestedServletException?
Our solution is rather a workaround: The exception is caught in advice and error body is returned as HTTP response. Here is how the mock works:
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setHandlerExceptionResolvers(withExceptionControllerAdvice())
.build();
private ExceptionHandlerExceptionResolver withExceptionControllerAdvice() {
final ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver() {
#Override
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(final HandlerMethod handlerMethod, final Exception exception) {
Method method = new ExceptionHandlerMethodResolver(TestAdvice.class).resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(new TestAdvice(), method);
}
return super.getExceptionHandlerMethod(handlerMethod, exception);
}
};
exceptionResolver.afterPropertiesSet();
return exceptionResolver;
}
Advice class:
#ControllerAdvice
public class TestAdvice {
#ExceptionHandler(Exception.class)
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Object exceptionHandler(Exception e) {
return new HttpEntity<>(e.getMessage());
}
}
After than, following test method passes successfully:
#Test
public void testException
mockMvc.perform(post("/exception/path"))
.andExpect(status().is5xxServerError())
.andExpect(content().string("Exception body"));
}
Easier way is to inject #ExceptionHandler into your Spring Test Context or it throws exception right in MockMvc.perform() just before .andExpect().
#ContextConfiguration(classes = { My_ExceptionHandler_AreHere.class })
#AutoConfigureMockMvc
public class Test {
#Autowired
private MockMvc mvc;
#Test
public void test() {
RequestBuilder requestBuilder = MockMvcRequestBuilders.post("/update")
.param("branchId", "13000")
.param("triggerId", "1");
MvcResult mvcResult = mvc.perform(requestBuilder)
.andExpect(MockMvcResultMatchers.status().is4xxClientError())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(__ -> Assert.assertThat(
__.getResolvedException(),
CoreMatchers.instanceOf(SecurityException.class)))
.andReturn();
}
That way MvcResult.getResolvedException() holds #Controller's exception!
https://stackoverflow.com/a/61016827/173149