How to set onStatus in Spring WebClient builder instance - java

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);
}
}

Related

How to write Junit 5 test cases for Spring integration dsl configuration class

Here I'm using Scatter-gather pattern and calling 3 sub-flows parallelly. Then gathering them and aggregating them. I need to write junit test cases for the configuration class, gateway and controller. I'm new to Spring Integration so kindly help me with this.
The code is as follows -
//Configuration class
#Configuration
public class IntegrationConfiguration {
#Autowired LoansServiceImpl loansService;
long dbId = new SequenceGenerator().nextId();
// Main flow
#Bean
public IntegrationFlow flow() {
return flow ->
flow.split()
.log()
.channel(c -> c.executor(Executors.newCachedThreadPool()))
.convert(LoanProvisionRequest.class)
.scatterGather(
scatterer ->
scatterer
.applySequence(true)
.recipientFlow(flow1())
.recipientFlow(flow2())
.recipientFlow(flow3()),
gatherer -> gatherer.releaseLockBeforeSend(true))
.log()
.aggregate(a -> a.outputProcessor(MessageGroup::getMessages))
.channel("output-flow");
}
// flow1
#Bean
public IntegrationFlow flow1() {
return integrationFlowDefination ->
integrationFlowDefination
.channel(c -> c.executor(Executors.newCachedThreadPool()))
.handle(
message -> {
try {
lionService.saveLionRequest(
(LionRequest) message.getPayload(), String.valueOf(dbId));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
});
}
// flow2
#Bean
public IntegrationFlow flow2() {
return integrationFlowDefination ->
integrationFlowDefination
.channel(c -> c.executor(Executors.newCachedThreadPool()))
.handle(
message ->
lionService.getData(
(LionRequest) message.getPayload(), SourceSystem.PROVISION))
.log();
}
// flow3
#Bean
public IntegrationFlow flow3() {
return integrationFlowDefination ->
integrationFlowDefination
.channel(c -> c.executor(Executors.newCachedThreadPool()))
.handle(
message ->
lionService.prepareCDRequest(
(LionRequest) message));
}
#Bean
public MessageChannel replyChannel() {
return MessageChannels.executor("output-flow", outputExecutor()).get();
}
#Bean
public ThreadPoolTaskExecutor outputExecutor() {
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setCorePoolSize(4);
pool.setMaxPoolSize(4);
return pool;
}
}
//Gateway interface
#MessagingGateway
public interface LionGateway {
#Gateway(requestChannel = "flow.input", replyChannel = "output-flow")
List<?> echo(LionRequest lionRequest);
}
//Controller class
#Autowired private LionGateway lionGateway;
#PostMapping(value = "/invoke-integration")
public String invokeIntegrationFlow(#RequestBody LionRequest lionRequest) {
String response = lionGateway.echo(lionRequest).toString();
return response;
}
There is no need in the #Configuration testing: it just registers beans. So, you probably need just concentrate on the interaction with those beans in your tests.
The Controller you can test via MockMvc: https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html#spring-mvc-test-framework.
Probably the #MessagingGateway could be just covered with the controller testing since it looks like you just call it from that #PostMapping method.
To test Spring Integration endpoints you need to decide how granualy you'd like to go. But probably our Testing Framework with the MockIntegration can help you: https://docs.spring.io/spring-integration/docs/current/reference/html/testing.html#test-context

How to use Spring WebClient to make a subsequent call with different header setting?

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()));
}
}

How to mock this webClient using JUnit?

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))
)));
}
}

ReactiveSecurityContextHolder#getContext returns an empty context

I try to authenticate user (it works) and get a user token from the context and it doesn't work.
I have a simple microservice application as a my pet-project and use a WebFlux as a web-framework. I tried to debug ReactiveSecurityContextHolder#getContext and inside flatMap I see my user token inside context, but in my application I steel have an empty context.
SecurityConfiguration.java
#EnableWebFluxSecurity
public class SecurityConfiguration {
#Bean
public SecurityWebFilterChain securityWebFilterChain(
AuthenticationWebFilter authenticationWebFilter,
ServerHttpSecurity http) {
return http
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.authorizeExchange()
.anyExchange().authenticated()
.and()
.build();
}
#Bean
public AuthenticationWebFilter authenticationWebFilter(
SecurityTokenBasedAuthenticationManager authenticationManager,
TokenAuthenticationConverter tokenAuthenticationConverter) {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(tokenAuthenticationConverter);
authenticationWebFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
return authenticationWebFilter;
}
}
ReactiveAuthenticationManager.java
#Slf4j
#Component
#AllArgsConstructor
public class SecurityTokenBasedAuthenticationManager implements ReactiveAuthenticationManager {
private final AuthWebClient authWebClient;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.switchIfEmpty(Mono.defer(this::raiseBadCredentials))
.cast(Authorization.class)
.flatMap(this::authenticateToken)
.map(user ->
new Authorization(user, (AuthHeaders) authentication.getCredentials()));
}
private <T> Mono<T> raiseBadCredentials() {
return Mono.error(new TokenValidationException("Invalid Credentials"));
}
private Mono<User> authenticateToken(Authorization authenticationToken) {
AuthHeaders authHeaders = (AuthHeaders) authenticationToken.getCredentials();
return Optional.of(authHeaders)
.map(headers -> authWebClient.validateUserToken(authHeaders.getAuthToken(), authHeaders.getRequestId())
.doOnSuccess(user -> log
.info("Authenticated user " + user.getUsername() + ", setting security context")))
.orElseThrow(() -> new MissingHeaderException("Authorization is missing"));
}
}
SecurityUtils.java
#Slf4j
public class SecurityUtils {
public static Mono<AuthHeaders> getAuthHeaders() {
return getSecurityContext()
.map(SecurityContext::getAuthentication)
.map(Authentication::getCredentials)
.cast(AuthHeaders.class)
.doOnSuccess(authHeaders -> log.info("Auth headers: {}", authHeaders));
}
private static Mono<SecurityContext> getSecurityContext() {
return ReactiveSecurityContextHolder.getContext();
}
}
WebClient building
(...)
WebClient.builder()
.baseUrl(url)
.filter(loggingFilter)
.defaultHeaders(httpHeaders ->
getAuthHeaders()
.doOnNext(headers -> httpHeaders.putAll(
Map.of(
REQUEST_ID, singletonList(headers.getRequestId()),
AUTHORIZATION, singletonList(headers.getAuthToken())))))
.build()
(...)
The main problem is inside a building of my WebClient - I expect, that I'll have complete webclient with requested header, but as I described above - I have an empty context inside SecurityUtils.java
So, after investigating and debugging I found a reason using combining streams into single one. IMHO, this is not quite reason for this issue, but now it works.
(...)
getAuthHeaders()
.flatMap(authHeaders -> buildWebClient(url, authHeaders)
.get()
(...)

Get request attribute with Spring webflux and WebTestClient

I'm not able to bind an attribute that I'm setting from a WebTestClient into a RestController when using Spring WebFlux.
I tried the two ways I could think of.
First using the #RequestAttribute annotation and I got:
Failed to handle request [GET /attributes/annotation]: Response status 400 with reason "Missing request attribute 'attribute' of type String"
Then I tried with the ServerWebExchange and was null.
This is my controller:
#RestController
#RequestMapping("/attributes")
public class MyController {
#GetMapping("/annotation")
public Mono<String> getUsingAnnotation(#RequestAttribute("attribute") String attribute) {
return Mono.just(attribute);
}
#GetMapping("/exchange")
public Mono<String> getUsingExchange(ServerWebExchange exchange) {
return Mono.just(exchange.getRequiredAttribute("attribute"));
}
}
And this is my failing test:
#RunWith(SpringRunner.class)
#SpringBootTest
public class MyControllerTest {
#Autowired
ApplicationContext context;
WebTestClient webClient;
#Before
public void setup() {
webClient = WebTestClient.bindToApplicationContext(context)
.configureClient()
.build();
}
#Test
public void testGetAttributeUsingAnnotation() {
webClient.get()
.uri("/attributes/annotation")
.attribute("attribute", "value")
.exchange()
.expectStatus()
.isOk();
}
#Test
public void testGetAttributeUsingExchange() {
webClient.get()
.uri("/attributes/exchange")
.attribute("attribute", "value")
.exchange()
.expectStatus()
.isOk();
}
}
In my real application I have a SecurityContextRepository that sets some attributes from a (decoded) header value and I'd like to get those attributes.
I've run into the same issue with a test which previously used MockMvc and then had to be converted to use WebClient. Like #jcfandino I was expecting the .attribute() methods on the WebClient to work similar to MockMvc's requestAttribute().
I haven't found out how .attribute() is meant to be used but I've bypassed the entire problem by adding a custom test filter. I'm not sure if this approach is correct but since this question has been unanswered the approach below may be of help for people running into the same issue.
#WebFluxTest(controllers = SomeController.class)
#ComponentScan({ "com.path1", "com.path2" })
class SomeControllerTest {
// define a test filter emulating the server's filter (assuming there is one)
private class AttributeFilter implements WebFilter {
String attributeValue;
public AttributeFilter(String filterAttributeValue) {
attributeValue = filterAttributeValue;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// add the desired attributes
exchange.getAttributes().put(SomeController.ATTR_NAME, attributeValue);
return chain.filter(exchange);
}
}
// mock the service the controller is dependend on
#MockBean
AppService appService;
// define the test where the controller handles a get() operation
#Test
void testMethod() {
// mock the app service
when(appService.executeService(anyString(), anyString())).thenAnswer(input -> {
// ... return some dummy appData
});
var testClient= WebTestClient.bindToController(new SomeController(appService))
.webFilter(new SomeControllerTest.AttributeFilter("someValue"))
.build();
try {
var response = testClient
.get()
.uri("someroute")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectBody(AppData.class);
} catch (Exception e) {
fail("exception caught in testMethod", e);
}
}
}
Both on the server and client side, request attributes should be seen as Map-like data structures that can be used to transfer information within the client/server (for filters, codecs, etc).
That information is not sent over the network.
If you want to send that information from the client to the server, you should take a look at request params or the request body itself.

Categories

Resources