Webflux #RequestBody return 400 BAD_REQUEST - spring-boot

When developing a controller webflux on post request in the parameters of the controller, I pass the request body using the annotation, but in webtestclient method return 400 BAD_REQUEST
My DTO:
#AllArgsConstructor
#Data
public class VisitRequest {
private String description;
public static Visit createEntityFromDto(Long customerId, Long deviceId, VisitRequest visitRequest) {
return new Visit(null, customerId, deviceId, visitRequest.getDescription(),
null);
}
}
My Controller:
#RestController
#RequestMapping(value = VisitController.REST_URL)
#AllArgsConstructor
#Slf4j
public class VisitController {
static final String REST_URL = "/api/customers/{customerId}/devices/{deviceId}";
private final VisitService visitService;
//POST http://visits-service/api/customers/{customerId}/devices/{deviceId}/visits
#PostMapping(value = "/visits", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<Visit> save(#PathVariable("customerId") #Validated long customerId,
#PathVariable("deviceId") #Validated long deviceId,
#RequestBody VisitRequest visitRequest) {
log.info("save {} for customer_id: {}, device_id {}", visitRequest, customerId, deviceId);
return visitService.save(createEntityFromDto(customerId, deviceId, visitRequest))
.switchIfEmpty(error(new RuntimeException("Bad request for save visit:" + visitRequest)));
}
}
My Test class with WebTestClient:
#ExtendWith(SpringExtension.class)
#SpringBootTest(classes = VisitApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class VisitControllerTest {
#LocalServerPort
private int port;
#Test
void save() throws Exception {
WebTestClient
.bindToServer()
.baseUrl("http://localhost:" + port)
.build()
.post()
.uri("/api/customers/" + ONE.getCustomerId()
+ "/devices/" + ONE.getDeviceId() + "/visits")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(new VisitRequest("test"))
.exchange()
.expectStatus().isCreated()
.expectHeader().valueEquals("Content-Type", MediaTypes.ALPS_JSON_VALUE);
}
}
When I run the test, WebTestClient return:
> POST http://localhost:63028/api/customers/10000/devices/20000/visits
> WebTestClient-Request-Id: [1]
> Content-Type: [application/json]
> Accept: [application/json]
> Content-Length: [22]
{"description":"test"}
< 400 BAD_REQUEST Bad Request
< Content-Type: [application/json]
< Content-Length: [169]
{"timestamp":"2020-09-27T19:28:09.987+00:00","path":"/api/customers/10000/devices/20000/visits","status":400,"error":"Bad Request","message":"","requestId":"42d54765-1"}
When I put a breakpoint on the logging line in the save method controller in the IntelliJ IDEA, and when I run the test in debug, the controller returns a bad request before reaching the breakpoint
Please help me find the cause of the error

Related

WebFlux API-Layer Test returns 404

I'm trying to get started with Spring WebFlux with Spring Boot 3.0
I'm Building a Person API with an open api generator.
The Application runs and gives the expected results when it is tested manually.
But I'm not able to get the API layer unit tested.
This is my Test Class
#WebFluxTest(controllers = {PersonApiController.class})
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = {PersonMapperImpl.class, H2PersonRepository.class, PersonRepository.class})
#DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class PersonRouterTest {
#MockBean
private PersonService personService;
#Autowired
private WebTestClient client;
#ParameterizedTest
#CsvSource({"1234, Max Mustermann", "5678, Erika Musterfrau"})
void retrieve_a_name(String id, String name) {
when(personService.getPersonDataByID(1234)).thenReturn(Mono.just(new PersonData(1234, "Max Mustermann")));
when(personService.getPersonDataByID(5678)).thenReturn(Mono.just(new PersonData(5678, "Erika Musterfrau")));
client.get()
.uri(uriBuilder -> uriBuilder
.path("/persons/{id}")
.build(id))
.accept(MediaType.ALL)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody()
.jsonPath("$.name").isEqualTo(name);
}
This is my Controller Class
#Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-12-
09T09:14:36.692713900+01:00[Europe/Vienna]")
#Controller
#RequestMapping("${openapi.openAPIDefinition.base-path:}")
public class PersonApiController implements PersonApi {
private final PersonApiDelegate delegate;
public PersonApiController(#Autowired(required = false) PersonApiDelegate delegate) {
this.delegate = Optional.ofNullable(delegate).orElse(new PersonApiDelegate() {});
}
#Override
public PersonApiDelegate getDelegate() {
return delegate;
}
}
The API interface:
#Tag(
name = "Person",
description = "the Person API"
)
public interface PersonApi {
default PersonApiDelegate getDelegate() {
return new PersonApiDelegate() {
};
}
#Operation(
operationId = "findPersonById",
summary = "Find Person by ID",
tags = {"Person"},
responses = {#ApiResponse(
responseCode = "200",
description = "successful operation",
content = {#Content(
mediaType = "application/json",
schema = #Schema(
implementation = PersonData.class
)
)}
)}
)
#RequestMapping(
method = {RequestMethod.GET},
value = {"/persons/{id}"},
produces = {"application/json"}
)
default Mono<ResponseEntity<PersonData>> findPersonById(#Parameter(name = "id",description = "Person ID",required = true) #PathVariable("id") Integer id, #Parameter(hidden = true) final ServerWebExchange exchange) {
return this.getDelegate().findPersonById(id, exchange);
}
#Operation(
operationId = "savePerson",
summary = "Creates a new Person",
tags = {"Person"},
responses = {#ApiResponse(
responseCode = "200",
description = "successful operatoin",
content = {#Content(
mediaType = "application/json",
schema = #Schema(
implementation = PersonData.class
)
)}
)}
)
#RequestMapping(
method = {RequestMethod.POST},
value = {"/persons"},
produces = {"application/json"},
consumes = {"application/json"}
)
default Mono<ResponseEntity<PersonData>> savePerson(#Parameter(name = "PersonData",description = "") #RequestBody(required = false) Mono<PersonData> personData, #Parameter(hidden = true) final ServerWebExchange exchange) {
return this.getDelegate().savePerson(personData, exchange);
}
}
and finally my delegate impl:
#Service
public class PersonDelegateImpl implements PersonApiDelegate {
public static final Mono<ResponseEntity<?>> RESPONSE_ENTITY_MONO = Mono.just(ResponseEntity.notFound().build());
private final PersonService service;
private final PersonMapper mapper;
public PersonDelegateImpl(PersonService service, PersonMapper mapper) {
this.service = service;
this.mapper = mapper;
}
public static <T> Mono<ResponseEntity<T>> toResponseEntity(Mono<T> mono) {
return mono.flatMap(t -> Mono.just(ResponseEntity.ok(t)))
.onErrorResume(t -> Mono.just(ResponseEntity.internalServerError().build()));
}
#Override
public Mono<ResponseEntity<PersonData>> findPersonById(Integer id, ServerWebExchange exchange) {
Mono<com.ebcont.talenttoolbackend.person.PersonData> personDataByID = service.getPersonDataByID(id);
return toResponseEntity(personDataByID.map(mapper::map));
}
#Override
public Mono<ResponseEntity<PersonData>> savePerson(Mono<PersonData> personData, ServerWebExchange exchange) {
return PersonApiDelegate.super.savePerson(personData, exchange);
If I run the test class I always get:
< 404 NOT_FOUND Not Found
< Content-Type: [application/json]
< Content-Length: [139]
{"timestamp":"2022-12-09T08:45:41.278+00:00","path":"/persons/1234","status":404,"error":"Not Found","message":null,"requestId":"4805b8b8"}
I have tried to change the Context Configuration but I did not get it to work.
I found the Problem, changing the Test Config to :
#WebFluxTest
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = {PersonMapperImpl.class, H2PersonRepository.class, PersonRepository.class, PersonApiController.class, PersonDelegateImpl.class})
#DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
Solved my Problem.
The Controller bean was not recognized. I had to add PersonApiCrontroller and PersonDelegateImpl to the Context Config. i then removed the PersonApiController from the #WebFluxTest annotation.

Getting error 404 instead of 200 in unit test

This is my CurriculoControllerTest.java class
#SpringBootTest
#ExtendWith(SpringExtension.class)
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
#DisplayName("Curriculo Controller Test")
#ActiveProfiles("local")
#AutoConfigureMockMvc
#Import(CurriculoController.class)
class CurriculoControllerTest {
private final String JSON_FORMAT = "application/json; charset=utf-8";
private final String BASE_PATH = "/curriculos";
#MockBean
private CurriculoServiceImpl curriculoService;
#SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
#Autowired
private MockMvc mockMvc;
public static CurriculoDTO createCurriculoInput() {
return CurriculoDTO.builder()
.id(UUID.randomUUID())
.dadosPessoais(DadosPessoaisDTO.builder()
.nome("joão")
.cargo("programador")
.email("joao#email.com")
.build())
.build();
}
CurriculoDTO novoCurriculo = CurriculoDTO.builder()
.id(UUID.randomUUID())
.dadosPessoais(DadosPessoaisDTO.builder()
.nome("Bruno")
.build())
.build();
CurriculoDTO curriculoExpected = CurriculoDTO.builder()
.id(UUID.randomUUID())
.dadosPessoais(DadosPessoaisDTO.builder()
.nome("Bruno")
.cargo("programador")
.email("joao#email.com")
.build())
.build();
#Test
#DisplayName("Deve retornar sucesso ao atualizar os dados pessoais do currículo")
public void deveRetornarSucessoAoAtualizarDadosPessoaisDoCurriculo() throws Exception {
var ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
var json = ow.writeValueAsString(curriculoExpected.getDadosPessoais());
doReturn(curriculoExpected).when(curriculoService)
.updateDadosPessoais(createCurriculoInput().getDadosPessoais(), novoCurriculo.getId());
mockMvc.perform(patch(BASE_PATH + "/dados-pessoais/" + createCurriculoInput().getId()).contentType(JSON_FORMAT).content(json))
.andExpect(status().isOk());
}
}
CurriculoController.java
#RestController
#RequestMapping("/curriculos")
public class CurriculoController {
private final DateTimeFormatter YYYY_MM_DD = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private final CurriculoServiceImpl service;
#Autowired
public CurriculoController(CurriculoServiceImpl service) {
this.service = service;
}
#PatchMapping("/dados-pessoais/{id}")
public ResponseEntity<CurriculoDTO> updateDadosPessoais(#RequestBody #Valid DadosPessoaisDTO dto,
#PathVariable UUID id) {
Optional<CurriculoDTO> curriculo = Optional.ofNullable(service.findById(id));
if (curriculo.isEmpty()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<>(service.updateDadosPessoais(dto, id), HttpStatus.OK);
}
CurriculoServiceImpl
public CurriculoDTO updateDadosPessoais(DadosPessoaisDTO dto, UUID id) {
Optional<Curriculo> optCurriculo = repository.findById(id)
.map(curriculo -> {
curriculo.setNome(Objects.nonNull(dto.getNome())
? dto.getNome() : curriculo.getNome());
curriculo.setCargo(Objects.nonNull(dto.getCargo())
? dto.getCargo() : curriculo.getCargo());
curriculo.setEmail(Objects.nonNull(dto.getEmail())
? dto.getEmail() : curriculo.getEmail());
curriculo.setSumario(Objects.nonNull(dto.getSumario())
? dto.getSumario() : curriculo.getSumario());
curriculo.setLinguagem(Objects.nonNull(dto.getLinguagem())
? dto.getLinguagem() : curriculo.getLinguagem());
return repository.save(curriculo);
});
CurriculoDTO curriculoDTO = converter.mapCurriculoToCurriculoDTO(optCurriculo.orElse(null));
curriculoDTO.setDadosPessoais(dto);
return curriculoDTO;
}
I've tried dozens of different ways, but I keep getting the 404 error, even though my URL is correct, could it be because the ID is not being found?
java.lang.AssertionError: Status expected:<200> but was:<404>
Expected :200
Actual :404
You mocked CurriculoServiceImpl but haven't stubbed service.findById(id) - you get an empty curriculo and return HttpStatus.NOT_FOUND.
As a side note - you seem to be testing only one controller mocking a service it depends on - you may want to consider #WebMvcTest instead of #SpringBootTest

How can I hide #ApiResponse form #ControllerAdvice for an endpoint?

I'm trying to migrate our manually writen OpenAPI (swagger) to a generated OpenAPI using springdoc-openapi for our Spring-Boot application. We got some issues, because the controller responses (mostly ErrorCodes) didn't match to the documentatation.
We already used a #ControllerAdvice annotated handler configuration. Here a snippet:
#ControllerAdvice
public class ExceptionHandler {
#ResponseStatus(code = HttpStatus.NOT_FOUND)
#ApiResponse(responseCode = "404", description = "(NOT FOUND) Resource does not exist!", content = #Content)
#ExceptionHandler(NotFoundException.class)
public void handleException(NotFoundException e) {
log.warn("Returning {} due to a NotFoundException: {}", HttpStatus.NOT_FOUND, e.toString());
}
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
#ApiResponse(responseCode = "400", description = "(BAD REQUEST) Given resource is invalid!", content = #Content)
#ExceptionHandler(InvalidResourceException.class)
public void handleException(InvalidResourceExceptione) {
log.error("Invalid resource: {}", e.toString());
}
The generated API now showed all defined ApiResponses as responses for all controllers and endpoints. So I splittet the handler config using #ControllerAdvice(basePackageClasses = MyController.class) to group the possible exceptions. But there are still responses that are not fitting to all endpoints of a controller. Like:
#RestController
public class MyController {
#ResponseStatus(HttpStatus.CREATED)
#Operation(summary = "Create", description = "Create myResource!")
#PostMapping(value = "/myResources/", produces = {"application/json"})
#ResponseBody
public Integer create(#RequestBody MyResource newResource) throws InvalidResourceException {
return creationService.createResource(newResource).getId();
}
#ResponseStatus(HttpStatus.OK)
#Operation(summary = "Update", description = "Update myResource!")
#PutMapping(value = "/myResources/{id}", produces = {"application/json"})
public void update(#PathVariable("id") Integer id, #RequestBody MyResource newResource)
throws ResourceNotFoundException, InvalidResourceException {
return updateService.updateResource(id, newResource);
}
#ResponseStatus(HttpStatus.OK)
#Operation(summary = "Get", description = "Get myResource!")
#GetMapping(value = "/myResources/{id}", produces = {"application/json"})
#ResponseBody
public MyResource get(#PathVariable("id") Integer id) throws ResourceNotFoundException {
return loadingService.getResource(id);
}
}
POST will never respond with my 'business' 404 and GET will never respond with my 'business' 400. Is it possible to annotate an endpoint, so that not possible response codes are hidden in the API?
I tried to override the responses, but didn't work as intended:
#ResponseStatus(HttpStatus.OK)
#Operation(summary = "Get", description = "Get myResource!")
#ApiResponses({#ApiResponse(responseCode = "200", description = "(OK) Returning myResource"),
#ApiResponse(responseCode = "404", description = "(NOT FOUND) Resource does not exist!")})
#GetMapping(value = "/myResources/{id}", produces = {"application/json"})
#ResponseBody
public MyResource get(#PathVariable("id") Integer id) throws ResourceNotFoundException {
return loadingService.getResource(id);
}
400 still shows up...
You need to remove the #ApiResponse from your #ControllerAdvice class and need to add the respective response in your controller class, as mentioned by you.
#ResponseStatus(HttpStatus.OK)
#Operation(summary = "Get", description = "Get myResource!")
#ApiResponses({#ApiResponse(responseCode = "200", description = "(OK) Returning myResource"),
#ApiResponse(responseCode = "404", description = "(NOT FOUND) Resource does not exist!")})
#GetMapping(value = "/myResources/{id}", produces = {"application/json"})
#ResponseBody
public MyResource get(#PathVariable("id") Integer id) throws ResourceNotFoundException {
return loadingService.getResource(id);
}

#WebMvcTest - How to configure security so the test can run?

I am trying to unit test my Spring Controller service that returns a String. I want to assert that the expected url name is correct but my test always returns a null url. I am using SpringRunner, #WebMvcTest and MockMvc for this.
#Slf4j
#Controller
public class CompanyInfoController {
private CompanyService companyService;
#Autowired
public CompanyInfoController(final CompanyService companyService) {
this.companyService = companyService;
}
#PreAuthorize("#someService.hasRole('" + Constants.MY_ROLE + "')")
#RequestMapping(value = "/companyInfo", method = RequestMethod.GET)
public String getCompanyInfo(final HttpServletRequest request, final
Model model) throws Exception {
log.debug("Getting Company Info");
final CompanyInfoDTO companyInformation = loadCompanyInfo(request);
model.addAttribute("companyInformation", companyInformation);
return "companyInfo";
}
private CompanyInfoDTO loadCompanyInfo(final HttpServletRequest request) throws Exception {
final Account someAccount= (Account)request.getAttribute("someAccount");
if (null != someAccount) {
final CompanyInfoDTO companyInformation = companyService.getCompanyInfo(someAccount);
return companyInformation;
} else {
throw new Exception("Unable to retrieve this account details.");
}
}
}
Controller Test -
#RunWith(SpringRunner.class)
#WebMvcTest(CompanyInfoController.class)
#Import(SecurityConfiguration.class)
public class CompanyInfoControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private CompanyService companyService;
#MockBean(name = "someService")
private SomeService someAuthorizationService;
#Before
public void setUp(){
Mockito.when(someAuthorizationService.hasRole(Constants.MY_ROLE))
.thenReturn(true);
}
#Test
#WithMockUser
public void canInvokeGetCompanyInfoServiceSuccessfully() throws Exception{
CompanyInfoDTO dto = new CompanyInfoDTO();
final Account mockAccount = new Account();
mockAccount.setId("12334");
Mockito.when(companyService.getCompanyInfo(Mockito.any(Account.class)))
.thenReturn(dto);
RequestBuilder request = get("/companyInfo")
.requestAttr("someAccount", mockAccount);
mockMvc.perform(get("/companyInfo"))
.andExpect(status().isOk());
}
#Test
#WithMockUser
public void forwardsToCompanyInfoPageSuccessfully() throws Exception{
CompanyInfoDTO dto = new CompanyInfoDTO();
final Account mockAccount = new Account();
mockAccount.setId("12334");
Mockito.when(companyService.getCompanyInfo(Mockito.any(Account.class)))
.thenReturn(dto);
RequestBuilder request = get("/companyInfo")
.requestAttr("someAccount", mockAccount);
mockMvc.perform(request)
.andExpect(redirectedUrl("companyInfo"));
}
}
The test keeps failing with this error -
java.lang.AssertionError: Redirected URL
Expected :companyInfo
Actual :null
Not sure what I am missing!
I do not see the debug statement inside my controller getting printed. But my controller is not mocked out. Is my security mocking correct? Any help is greatly appreciated.
Update - This is what I see in my logs -
MockHttpServletRequest:
HTTP Method = GET
Request URI = /companyInfo
Parameters = {}
Headers = []
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = org.cmt.controller.CompanyInfoController
Method = public java.lang.String org.cmt.controller.CompanyInfoController.getCompanyInfo(javax.servlet.http.HttpServletRequest,org.springframework.ui.Model) throws java.lang.Exception
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", X-Frame-Options:"SAMEORIGIN"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
java.lang.AssertionError: No ModelAndView found
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:36)
I see Http response code 200. I am assuming it authenticates just fine. But why would my model and view be null?

#PathVariable Validation in Spring 4

How can i validate my path variable in spring. I want to validate id field, since its only single field i do not want to move to a Pojo
#RestController
public class MyController {
#RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public ResponseEntity method_name(#PathVariable String id) {
/// Some code
}
}
I tried doing adding validation to the path variable but its still not working
#RestController
#Validated
public class MyController {
#RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public ResponseEntity method_name(
#Valid
#Nonnull
#Size(max = 2, min = 1, message = "name should have between 1 and 10 characters")
#PathVariable String id) {
/// Some code
}
}
You need to create a bean in your Spring configuration:
#Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
You should leave the #Validated annotation on your controller.
And you need an Exceptionhandler in your MyController class to handle theConstraintViolationException :
#ExceptionHandler(value = { ConstraintViolationException.class })
#ResponseStatus(value = HttpStatus.BAD_REQUEST)
public String handleResourceNotFoundException(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
StringBuilder strBuilder = new StringBuilder();
for (ConstraintViolation<?> violation : violations ) {
strBuilder.append(violation.getMessage() + "\n");
}
return strBuilder.toString();
}
After those changes you should see your message when the validation hits.
P.S.: I just tried it with your #Size validation.
To archive this goal I have apply this workaround for getting a response message equals to a real Validator:
#GetMapping("/check/email/{email:" + Constants.LOGIN_REGEX + "}")
#Timed
public ResponseEntity isValidEmail(#Email #PathVariable(value = "email") String email) {
return userService.getUserByEmail(email).map(user -> {
Problem problem = Problem.builder()
.withType(ErrorConstants.CONSTRAINT_VIOLATION_TYPE)
.withTitle("Method argument not valid")
.withStatus(Status.BAD_REQUEST)
.with("message", ErrorConstants.ERR_VALIDATION)
.with("fieldErrors", Arrays.asList(new FieldErrorVM("", "isValidEmail.email", "not unique")))
.build();
return new ResponseEntity(problem, HttpStatus.BAD_REQUEST);
}).orElse(
new ResponseEntity(new UtilsValidatorResponse(EMAIL_VALIDA), HttpStatus.OK)
);
}

Resources