Using WebClient in a controller tested with #WebFluxTests throws java.lang.IllegalArgumentException: URI is not absolute - spring

I have a #RestController that uses WebClient in one of its endpoints to invoke another endpoint from the same controller:
#RestController
#RequestMapping("/api")
#RequiredArgsConstructor
public class FooRestController {
private final WebClient webClient;
#Value("${service.base-url}")
private String fooServiceBaseUrl;
#GetMapping(value = "/v1/foo", produces = MediaType.APPLICATION_JSON_VALUE)
public Flux<Foo> getFoo() {
return webClient.get().uri(fooServiceBaseUrl + "/api/v1/fooAnother")
.retrieve()
.bodyToFlux(Foo.class);
}
#GetMapping(value = "/v1/fooAnother", produces = MediaType.APPLICATION_JSON_VALUE)
public Flux<Foo> getFooAnother() {
return Flux.xxx
}
In my #WebFluxTests class I can test the fooAnother endpoint without any problem:
#ExtendWith(SpringExtension.class)
#Import({MyWebClientAutoConfiguration.class})
#WebFluxTest(FooRestController.class)
class FooRestControllerTest {
#Test
void shouldGetFooAnother() {
xxx
webTestClient.get()
.uri("/api/v1/fooAnother")
.exchange()
.expectStatus().isOk()
}
#Test
void shouldGetFoo() {
xxx
webTestClient.get()
.uri("/api/v1/fooAnother")
.exchange()
.expectStatus().isOk()
}
However when I test the /v1/foo endpoint (notice in my tests service.base-url=""), it fails calling webClient.get().uri(fooServiceBaseUrl + "/api/v1/fooAnother") having fooServiceBaseUrl + "/api/v1/fooAnother" = "/api/v1/fooAnother", complaining that it need an absolute URL: java.lang.IllegalArgumentException: URI is not absolute: /api/v1/fooAnother.
How could I fix this test?

You have to configure your WebClient using WebClient.Builder(). You could do this inside your FooRestController but I like to use Configuration that way if you have any further WebClient customizations, you could do in different class rather than in your controller class.
Configure WebClient:
#Configuration
public class WebClientConfig() {
#Value("${service.base-url}")
private String fooServiceBaseUrl;
#Bean
public WebClient webClient(WebClient.Builder builder) {
return builder
.baseUrl(fooServiceBaseUrl)
.build();
}
}
If you decide to go ahead with configuring your webClient in your FooRestController, you have to refactor as below. You don't need above configuration.
If this doesn't solve your issue, you might have some sort of mismatch between application.yml file and the value that your are trying to inject in fooServiceBaseUrl.
#RestController
#RequestMapping("/api")
public class FooRestController() {
private final WebClient webClient;
#Value("${service.base-url}")
private String fooServiceBaseUrl;
public FooRestController(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder
.baseUrl(fooServiceBaseUrl)
.build();
}
#GetMapping(value = "/v1/foo", produces = MediaType.APPLICATION_JSON_VALUE)
public Flux<Foo> getFoo() {
return webClient
.get()
.uri("/api/v1/fooAnother")
.retrieve()
.bodyToFlux(Foo.class);
}
#GetMapping(value = "/v1/fooAnother", produces = MediaType.APPLICATION_JSON_VALUE)
public Flux<Foo> getFooAnother() {
return Flux.xxx
}
}

Related

Using Spring Boot WebClient to call a dummy api to postman

I am missing something here. I am attempting to pull information using Spring Boot WebClient from a Dummy Api that's an Http request. I am not getting any info pulled when I go into postman.
Thanks for any insight you can give me. I am still very new to coding and self-taught.
Here's my employee controller:
#Autowired
WebClientApp webClientApp;
#GetMapping("/consume")
public String getEmployee(Model model) {
model.addAttribute("listEmployees", empServiceImpl.getAllEmployees());
model.addAttribute("listemps", webClientApp.webClientBuilder());
return "index";
}
Web Client
private WebClient webClient;
public void SimpleWebClient(WebClient webClient) {
this.webClient = webClient;
}
public Flux<Employee> webClientBuilder() {
return this.webClient
//this.webClientBuilder = webClientBuilder.baseUrl(DummyEmployee)
.get()
.uri("api/v1/employees")
.retrieve()
.bodyToFlux(Employee.class);
}
Employee
#Data
#ToString
//#AllArgsConstructor
//#NoArgsConstructor
#JsonRootName(value = "data")
public class Employee {
#JsonProperty("id")
public int employeeID;
#JsonProperty("employee_name")
public String employeeName;
#JsonProperty("employee_salary")
public String employeeSalary;
#JsonProperty("employee_age")
public int employeeAge;
#JsonProperty("employee_image")
public Blob employeeImage;
}
Service
#Repository
#ComponentScan(basePackages = {"com.example.app.repository"})
#Service
public class ServiceImpl implements EmpService{
#Autowired
private EmployeeRepository employeeRepo;
#SuppressWarnings("unchecked")
public List<Employee> getAllEmployees() {
return (List<Employee>) employeeRepo.findAll();
}
}
Service
#Service
public interface EmpService {
static List<Employee> getAllEmployees() {
// TODO Auto-generated method stub
return null;
}
}
Main
public static void main(String[] args) {
SpringApplication.run(RestWebsiteDataProjectApplication.class, args);
}
#Bean
public WebClient webClientFromScratch() {
return WebClient.builder()
.baseUrl("https://dummy.restapiexample.com/")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
Flux only emits its content when it is subscribed. You are not subscribing to the Flux returned by the webClientBuilder() method.
You shouldn't really do this, but try adding .block() to your Controller as follows:
#Autowired
WebClientApp webClientApp;
#GetMapping("/consume")
public String getEmployee(Model model) {
model.addAttribute("listEmployees", empServiceImpl.getAllEmployees());
model.addAttribute("listemps", webClientApp.webClientBuilder().block());
return "index";
}
If this works, please consider reworking your code because while working with Spring WebFlux (reactive programming) you should always deal with Mono and Flux so that you can take full advantage of the reactive stack.

#MockBean with Junit Jupiter concurrent mode

I am trying to use
junit.jupiter.execution.parallel.mode.default=concurrent
along with #MockBean from Spring Boot. However, the tests starts to fail when I set to concurrent mode. I tried setting
#MockBean(reset = MockReset.NONE)
However, it also does not help. Seems like mocked bean is reinitialized / reset even though MockReset.NONE is set.
Is that possible to use #MockBean allow with concurrent mode or is it a known limitation?
Bean I am mocking:
#Service
public class SampleService {
public Mono<String> processCall(String call) {
return Mono.just("ok");
}
}
The controller I am testing:
#RestController
#RequiredArgsConstructor
public class SampleController {
private final SampleService sampleService;
#PostMapping("/call")
public Mono<String> processCall(#RequestBody String body) {
return sampleService.processCall(body);
}
}
And tests:
#ExtendWith(SpringExtension.class)
#WebFluxTest(SampleController.class)
#ContextConfiguration(classes = {SampleController.class})
class SampleTest {
#MockBean(reset = MockReset.NONE)
private SampleService sampleService;
#Autowired
private WebTestClient webTestClient;
#Test
void givenSampleServiceWorksFineExpectOkResponse() {
String randomBody = "1234";
when(sampleService.processCall(randomBody)).thenReturn(Mono.just("ok"));
callService(randomBody).expectStatus().isOk().expectBody(String.class).isEqualTo("ok");
}
#Test
void givenSampleServiceFailedExpectErrorResponse() {
String randomBody = "9876";
when(sampleService.processCall(randomBody))
.thenReturn(Mono.error(new RuntimeException("error")));
callService(randomBody).expectStatus().is5xxServerError();
}
private ResponseSpec callService(String body) {
return webTestClient
.post()
.uri(
uriBuilder ->
uriBuilder
.path("/call")
.build())
.bodyValue(body)
.exchange();
}
}
I set concurrent mode for Jupiter in file junit-platform.properties:
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.config.strategy=dynamic

Spring, webflux: The getRemoteAddress method of the ServerHttpRequest object returns null when request performed from WebTestClient

I have a controller
#RestController
public class NameController {
#Autowired
private NameService nameService;
#GetMapping("/name")
public Mono<UploadParamsDto> getName(ServerHttpRequest request) {
return nameService.getNameByHost(request.getRemoteAddress().getHostName());
}
}
and i have a test method:
#ExtendWith(SpringExtension.class)
#WebFluxTest(NameControllerTest.class)
#ActiveProfiles("test")
class NameControllerTest {
#Autowired
private WebTestClient webClient;
#Test
void nameTest() {
webClient.get().uri("/name")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isOk();
}
}
When I run the test in order to check my getName method i got NPE because
request.getRemoteAddress() returns null, could you please tell me how to mock ServerHttpRequest? (I know that there is MockServerHttpRequest, but I couldn't managed with it)
My purpose is make request.getRemoteAddress().getHostName() return mock value.
Thanks to everyone.
Works in next way:
#ExtendWith(SpringExtension.class)
#WebFluxTest(NameControllerTest.class)
#ActiveProfiles("test")
class NameControllerTest {
#Autowired
private ApplicationContext context;
#Test
void nameTest() {
WebTestClient webClient = WebTestClient
.bindToApplicationContext(context)
.webFilter(new SetRemoteAddressWebFilter("127.0.0.1"))
.configureClient()
.build();
webClient.get().uri("/name")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus()
.isOk();
}
}
Where SetRemoteAddressWebFilter is WebFilter:
public class SetRemoteAddressWebFilter implements WebFilter {
private String host;
public SetRemoteAddressWebFilter(String host) {
this.host = host;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(decorate(exchange));
}
private ServerWebExchange decorate(ServerWebExchange exchange) {
final ServerHttpRequest decorated = new ServerHttpRequestDecorator(exchange.getRequest()) {
#Override
public InetSocketAddress getRemoteAddress() {
return new InetSocketAddress(host, 80);
}
};
return new ServerWebExchangeDecorator(exchange) {
#Override
public ServerHttpRequest getRequest() {
return decorated;
}
};
}
}
Running a test with #WebFluxTest doesn't involve a real server, you've figured that out.
But getting a NullPointerException doesn't feel right still - could you create an issue on https://jira.spring.io about that? I don't think you should have to work around this, but Spring Framework should probably provide some infrastructure to "mock" that information.

Spring boot - integration testing - WebTestClient & HttpServletRequest

I'm having difficulties figuring this out.
I can mock almost everything but for some reason the HttpServletRequest is mocked but not injected into the #ControllerAdvice #ExceptionHandler method.
Any ideas? Thank you for your help in advance!
STR Repo with minimal plug and play test suite / code
https://github.com/krodyrobi/spring-integration-test-str
#Component
public class Config {
private final String url = "https://httpstat.us/";
public String getUrl() {
return url;
}
}
#RestControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(WebClientResponseException.class)
public ResponseEntity<String> handleException(HttpServletRequest request, WebClientResponseException ex) {
return new ResponseEntity<>(request.getRequestURL() + " " + ex.getResponseBodyAsString(), ex.getStatusCode());
}
}
#RestController
public class SomeController {
private final Config config;
#Autowired
public SomeController(Config config) {
this.config = config;
}
#GetMapping("/test")
public Mono<String> test() {
return WebClient
.create(config.getUrl())
.get()
.uri("/200")
.retrieve()
.bodyToMono(String.class);
}
}
#RunWith(SpringRunner.class)
#SpringBootTest(classes = {
SomeController.class,
GlobalExceptionHandler.class,
})
public class SomeControllerTest {
private final static String baseUrl = "http://localhost:9999/";
public #Rule WireMockRule wireMockRule = new WireMockRule(9999);
private #MockBean Config config;
private #MockBean HttpServletRequest request;
private WebTestClient webClient;
private #Autowired SomeController controller;
private #Autowired GlobalExceptionHandler exceptionHandler;
#Before
public void setUp() {
webClient = WebTestClient
.bindToController(controller)
.controllerAdvice(exceptionHandler)
.build();
when(config.getUrl()).thenReturn(baseUrl);
}
#Test
public void test_works() {
wireMockRule
.stubFor(get(urlEqualTo("/200"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "text/plain")
.withBody("200 MOCK")));
webClient
.get()
.uri("/test")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("200 MOCK");
wireMockRule.verify(getRequestedFor(urlMatching("/200")));
}
#Test
public void test_fails() {
// java.lang.IllegalStateException: No suitable resolver for argument 0
// of type 'javax.servlet.http.HttpServletRequest' on public
// org.springframework.http.ResponseEntity<java.lang.String>
// com.example.demo.GlobalExceptionHandler.handleException(
// javax.servlet.http.HttpServletRequest,
// ...client.WebClientResponseException
// )
wireMockRule
.stubFor(get(urlEqualTo("/200"))
.willReturn(aResponse()
.withStatus(404)
.withHeader("Content-Type", "text/plain")
.withBody("404 MOCK")));
webClient
.get()
.uri("/test")
.exchange()
.expectStatus()
.isNotFound()
.expectBody(String.class)
.isEqualTo("Http://localhost:8080/test 404 MOCK");
wireMockRule.verify(getRequestedFor(urlMatching("/200")));
}
}
Use below instead of HttpServletRequest
import org.springframework.http.server.reactive.ServerHttpRequest;
ServerHttpRequest request

How to consume protobuf parameters using Spring REST?

I'm trying to pass a protobuf parameter to a REST endpoint but I get
org.springframework.web.client.HttpServerErrorException: 500 null
each time I try. What I have now is something like this:
#RestController
public class TestTaskEndpoint {
#PostMapping(value = "/testTask", consumes = "application/x-protobuf", produces = "application/x-protobuf")
TestTaskComplete processTestTask(TestTask testTask) {
// TestTask is a generated protobuf class
return generateResult(testTask);
}
}
#Configuration
public class AppConfiguration {
#Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
}
#SpringBootApplication
public class JavaConnectorApplication {
public static void main(String[] args) {
SpringApplication.run(JavaConnectorApplication.class, args);
}
}
and my test looks like this:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest
#WebAppConfiguration
public class JavaConnectorApplicationTest {
#Configuration
public static class RestClientConfiguration {
#Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
return new RestTemplate(Arrays.asList(hmc));
}
#Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
}
#Autowired
private RestTemplate restTemplate;
private int port = 8081;
#Test
public void contextLoaded() {
TestTask testTask = generateTestTask();
final String url = "http://127.0.0.1:" + port + "/testTask/";
ResponseEntity<TestTaskComplete> customer = restTemplate.postForEntity(url, testTask, TestTaskComplete.class);
// ...
}
}
I'm sure that it is something with the parameters because if I create a variant which does not take a protobuf parameter but returns one it just works fine. I tried debugging the controller code but the execution does not reach the method so the problem is probably somewhere else. How do I correctly parametrize this REST method?
This is my first stack overflow answer but I was a lot to frustred from searching for working examples with protobuf over http and spring.
the answer https://stackoverflow.com/a/44592469/15705964 from Jorge is nearly correct.
Like the comments mention: "This won't work in itself. You need to add a converter somewhere at least."
Do it like this:
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Autowired
ProtobufHttpMessageConverter protobufHttpMessageConverter;
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(protobufHttpMessageConverter);
}
}
The ProtobufHttpMessageConverter will do his job automatically and add the object to your controller methode
#RestController
public class ProtobufController {
#PostMapping(consumes = "application/x-protobuf", produces = "application/x-protobuf")
public ResponseEntity<TestMessage.Response> handlePost(#RequestBody TestMessage.Request protobuf) {
TestMessage.Response response = TestMessage.Response.newBuilder().setQuery("This is a protobuf server Response")
.build();
return ResponseEntity.ok(response);
}
Working example with send and reseive with rest take a look: https://github.com/Chriz42/spring-boot_protobuf_example
Here it's the complete answer
#SpringBootApplication
public class JavaConnectorApplication {
public static void main(String[] args) {
SpringApplication.run(JavaConnectorApplication.class, args);
}
}
Then you need to provide the right configuration.
#Configuration
public class AppConfiguration {
//You need to add in this list all the messageConverters you will use
#Bean
RestTemplate restTemplate(ProtobufHttpMessageConverter hmc) {
return new RestTemplate(Arrays.asList(hmc,smc));
}
#Bean
ProtobufHttpMessageConverter protobufHttpMessageConverter() {
return new ProtobufHttpMessageConverter();
}
}
And finally your RestController.
#RestController
public class TestTaskEndpoint {
#PostMapping(value = "/testTask")
TestTaskComplete processTestTask(#RequestBody TestTask testTask) {
// TestTask is a generated protobuf class
return generateResult(testTask);
}
}
The #RequestBody annotation: The body of the request is passed through an HttpMessageConverter (That you already defined) to resolve the method argument depending on the content type of the request
And your test class:
#RunWith(SpringRunner.class)
#SpringBootTest
#WebAppConfiguration
public class JavaConnectorApplicationTest {
#Autowired
private RestTemplate restTemplate;
private int port = 8081;
#Test
public void contextLoaded() {
TestTask testTask = generateTestTask();
final String url = "http://127.0.0.1:" + port + "/testTask/";
ResponseEntity<TestTaskComplete> customer = restTemplate.postForEntity(url, testTask, TestTaskComplete.class);
// Assert.assertEquals("dummyData", customer.getBody().getDummyData());
}
}

Resources