Spring MVC Test with Validation JSR-303/349: Locale or accept-language does not work in testing - spring

I am working with Spring Framework 4.3.3
I have the following situation for a #Controller either for POST or PUT
For example for POST I have the following:
#PostMapping(value="/save",
consumes=MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces=MediaType.TEXT_HTML_VALUE)
public String saveOne(#Validated #ModelAttribute Persona persona,
BindingResult result,
RedirectAttributes redirectAttributes){
logger.info("saveOne: {}", persona.toString());
if(result.hasErrors()){
logger.error("# Errors: {}", result.getErrorCount());
logger.error("result: {}", result.toString());
return "persona/saveOne";
}
personaRestController.saveOne(persona);
redirectAttributes.addFlashAttribute("message", "process.successful");
return "redirect:/message";
}
The app works how is expected, through a Web Form this #PostMapping either saves/persists the data or validates the data and if it is invalid the same web form is shown with the error messages for each field and based about i18n such as English, Spanish and Portuguese. It related with JSR-439
About Java config infrastructure configuration I have the following:
#Bean
public LocaleChangeInterceptor localeChangeInterceptor(){
LocaleChangeInterceptor localeChangeInterceptor=new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("language");
//localeChangeInterceptor.setHttpMethods("post", "put");
return localeChangeInterceptor;
}
#Bean(name="localeResolver")
public LocaleResolver cookieLocaleResolver(){
CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
cookieLocaleResolver.setCookieName("language");
cookieLocaleResolver.setDefaultLocale(new Locale("en","US"));
return cookieLocaleResolver;
}
And
#EnableWebMvc
#Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
...
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptorConfig.localeChangeInterceptor());
}
The problem is about Spring MVC Test. I have the following:
#Before
public void setUp(){
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
...
#Test
public void saveOneHtmlPostInvalidMaxTest() throws Exception {
logger.info("saveOneHtmlPostInvalidMaxTest");
resultActions = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.locale(locale)
.header("accept-language", locale.toString())
.header("language", locale.toString())
.param("id", personaInvalidMax.getId())
.param("nombre", personaInvalidMax.getNombre())
.param("apellido", personaInvalidMax.getApellido())
....
.andDo(print());
Note observe:
.locale(locale)
.header("accept-language", locale.toString())
.header("language", locale.toString())
Even when some of them have no sense, they are there because I am trying to resolve the following situation:
The validation control from the server side works how is expected until some point.
First
I can confirm through .andDo(print()) method that MockHttpServletRequest prints
Headers = {Content-Type=[application/x-www-form-urlencoded],
accept-language=[pt_BR], language=[pt_BR]}
Observe pt_BR
Second
The #PostMapping that contains:
if(result.hasErrors()){
logger.error("# Errors: {}", result.getErrorCount());
logger.error("result: {}", result.toString());
return "persona/saveOne";
}
Prints...
7370 [Test worker] ERROR PersonaSaveOneController - result:
org.springframework.validation.BeanPropertyBindingResult: 4 errors
Field error in object 'persona' on field 'id': rejected value [0];
codes [Size.persona.id,Size.id,Size.java.lang.String,Size]; arguments
[org.springframework.context.support.DefaultMessageSourceResolvable:
codes [persona.id,id]; arguments []; default message [id],3,3];
default message [The size for the field must be between 3 and 3]
Observe the default message [id],3,3];
default message [The size for the field must be between 3 and 3]
Here two problems:
The locale or accept-language are ignored
The error messages prints always in English
What I need is that the error data prints for example in Portuguese and Spanish according the Locale sent. It should returns the error messages for each field in an expected language, in this case Portuguese
Again, it works when I use directly the Web Form, but not through the Test. I am assuming that exists an internal Spring component that works through production and has not been defined for Testing
Therefore, what is need it to accomplish this approach?
BTW: With the following (see I use classpath:), furthermore my .properties file are located in src/main/resources
#Bean(name="messageSource")
public ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource() {
ReloadableResourceBundleMessageSource resource = new ReloadableResourceBundleMessageSource();
resource.setBasenames(...
"classpath:/com/manuel/jordan/validation/validation");
resource.setDefaultEncoding("UTF-8");
return resource;
}
My #Test methods working around with Rest works fine about show and returning the error message according the Locale or accept-language defined (Access to MessageSource happens in the server side and works fine with the previous configuration). It just fails for non-Rest controllers

Related

Validating if request body in HTTP POST request is null in Spring Boot controller

I am replacing manual validation of input to a POST request in a Spring Boot REST-controller. JSR-303 Spring Bean Validation is used for validating the instance variables in the request body and this is working as expected. What is the recommended method to validate that the object in the request body is not null?
I have tried:
annotating the entire object such as this: #NotNull #Valid #RequestBody Foo foo
annotating the entire class with #NotNull
I am replacing:
#PostMapping...
public ResponseEntity<Map<String, Object>> editFoo(
#RequestBody Foo foo, ...) {
if(foo == null) {
return (new ResponseEntity<>(headers, HttpStatus.BAD_REQUEST));
}
}
with a Bean Validation equivalent:
#PostMapping...
public ResponseEntity<Map<String, Object>> editFoo(
#Valid #RequestBody Foo foo, ...) {
...
}
I tried unit testing the controller method by:
// Arrange
Foo foo = null;
String requestBody = objectMapper.writeValueAsString(foo);
// Act + assert
mockMvc
.perform(
post("/end_point")
.contentType("application/json")
.content(requestBody))
.andExpect(status().isBadRequest());
I expected a MethodArgumentNotValidException which is handled by a #ControllerAdvice for this exception, but I get HttpMessageNotReadableException when executing the unit test.
My questions:
is it necessary to test if the request body is null?
if 1. is true, how should this be done with Bean Validation?
Seeing your code, you already check if the body is null. In fact #RequestBody has a default parameter required which defaults to true. So no need for Bean validation for that !
Your main issue here seems to be in your test. First of all it is good to write a test to validate your endpoint behavior on null.
However, in your test you does not pass null. You try to create a Json object from a null value with your objectMapper.
The object you are writting seems not to be a valid json. So when your sending this body, Spring says that it cannot read the message, aka the body of your request, as you say it is a application/json content but there is not json in it.
To test null body, just send your request in your test just removing the .content(requestBody) line and it should work !
--- Edit 1
I thought it was rejecting the message because of the body, but in fact it seems to work right away for me. Here is my controler and test so you can compare to your full code :
#RestController()
#RequestMapping("end_point")
public class TestController {
#PostMapping
public ResponseEntity<Map<String, Object>> editFoo(#RequestBody Foo foo) {
// if(foo == null) {
// return (new ResponseEntity<>(new HashMap<>(), HttpStatus.BAD_REQUEST));
// }
return (new ResponseEntity<>(new HashMap<>(), HttpStatus.OK));
}
}
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutoConfigureMockMvc
public class TestControllerTest {
#Autowired
private MockMvc mvc;
#Autowired
private ObjectMapper objectMapper;
#Test
public void test_body_is_null() throws Exception {
Foo foo = null;
String requestBody = objectMapper.writeValueAsString(foo);
// Act + assert
mvc
.perform(
post("/end_point")
.contentType("application/json")
.content(requestBody))
.andExpect(status().isBadRequest());
}
}
This was made using Spring Boot 2.1.6.RELEASE
--- Edit 2
For the record if you want to use validation for null here, here is a snippet of the controller :
#RestController()
#RequestMapping("end_point")
#Validated
public class TestController {
#PostMapping
public ResponseEntity<Map<String, Object>> editFoo(#NotNull #RequestBody(required = false) Foo foo) {
return (new ResponseEntity<>(new HashMap<>(), HttpStatus.OK));
}
}
First you have to set required to false for the body, as default is true. Then you have to add the #NotNull annotation on the request body and #Validated on the controller.
Here if you launch your test you will see that the request fails with :
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is javax.validation.ConstraintViolationException: editFoo.foo: must not be null
As you said you had a #ControllerAdvice you can then map the exception as you wish !

Cannot properly test ErrorController Spring Boot

due to this tutorial - https://www.baeldung.com/spring-boot-custom-error-page I wanted to customize my error page ie. when someone go to www.myweb.com/blablablalb3 I want to return page with text "wrong url request".
All works fine:
#Controller
public class ApiServerErrorController implements ErrorController {
#Override
public String getErrorPath() {
return "error";
}
#RequestMapping("/error")
public String handleError() {
return "forward:/error-page.html";
}
}
But I dont know how to test it:
#Test
public void makeRandomRequest__shouldReturnErrorPage() throws Exception {
this.mockMvc.perform(get(RANDOM_URL))
.andDo(print());
}
print() returns:
MockHttpServletResponse:
Status = 404
Error message = null
Headers = {X-Application-Context=[application:integration:-1]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
So I cant created something like this:
.andExpect(forwardedUrl("error-page"));
because it fails, but on manual tests error-page is returned.
Testing of a custom ErrorController with MockMvc is unfortunately not supported.
For a detailed explanation, see the official recommendation from the Spring Boot team (source).
To be sure that any error handling is working fully, it's necessary to
involve the servlet container in that testing as it's responsible for
error page registration etc. Even if MockMvc itself or a Boot
enhancement to MockMvc allowed forwarding to an error page, you'd be
testing the testing infrastructure not the real-world scenario that
you're actually interested in.
Our recommendation for tests that want to be sure that error handling
is working correctly, is to use an embedded container and test with
WebTestClient, RestAssured, or TestRestTemplate.
My suggestion is to use #ControllerAdvice
In this way you can work around the problem and you can continue to use MockMvc with the big advantage that you are not required to have a running server.
Of course to test explicitly the error page management you need a running server. My suggestion is mainly for those who implemented ErrorController but still want to use MockMvc for unit testing.
#ControllerAdvice
public class MyControllerAdvice {
#ExceptionHandler(FileSizeLimitExceededException.class)
public ResponseEntity<Throwable> handleFileException(HttpServletRequest request, FileSizeLimitExceededException ex) {
return new ResponseEntity<>(ex, HttpStatus.PAYLOAD_TOO_LARGE);
}
#ExceptionHandler(Throwable.class)
public ResponseEntity<Throwable> handleUnexpected(HttpServletRequest request, Throwable throwable) {
return new ResponseEntity<>(throwable, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

spring boot override default REST exception handler

I am not able to override default spring boot error response in REST api. I have following code
#ControllerAdvice
#Controller
class ExceptionHandlerCtrl {
#ResponseStatus(value=HttpStatus.UNPROCESSABLE_ENTITY, reason="Invalid data")
#ExceptionHandler(BusinessValidationException.class)
#ResponseBody
public ResponseEntity<BusinessValidationErrorVO> handleBusinessValidationException(BusinessValidationException exception){
BusinessValidationErrorVO vo = new BusinessValidationErrorVO()
vo.errors = exception.validationException
vo.msg = exception.message
def result = new ResponseEntity<>(vo, HttpStatus.UNPROCESSABLE_ENTITY);
result
}
Then in my REST api I am throwing this BusinessValidationException. This handler is called (I can see it in debugger) however I still got default spring boot REST error message. Is there a way to override and use default only as fallback? Spring Boot version 1.3.2 with groovy. Best Regards
Remove #ResponseStatus from your method. It creates an undesirable side effect and you don't need it, since you are setting HttpStatus.UNPROCESSABLE_ENTITY in your ResponseEntity.
From the JavaDoc on ResponseStatus:
Warning: when using this annotation on an exception class, or when setting the reason attribute of this annotation, the HttpServletResponse.sendError method will be used.
With HttpServletResponse.sendError, the response is considered complete and should not be written to any further. Furthermore, the Servlet container will typically write an HTML error page therefore making the use of a reason unsuitable for REST APIs. For such cases it is preferable to use a ResponseEntity as a return type and avoid the use of #ResponseStatus altogether.
I suggest you to read this question: Spring Boot REST service exception handling
There you can find some examples that explain how to combine ErrorController/ ControllerAdvice in order to catch any exception.
In particular check this answer:
https://stackoverflow.com/a/28903217/379906
You should probably remove the annotation #ResponseStatus from the method handleBusinessValidationException.
Another way that you have to rewrite the default error message is using a controller with the annotation #RequestMapping("/error"). The controller must implement the ErrorController interface.
This is the error controller that I use in my app.
#RestController
#RequestMapping("/error")
public class RestErrorController implements ErrorController
{
private final ErrorAttributes errorAttributes;
#Autowired
public MatemoErrorController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}
#Override
public String getErrorPath() {
return "/error";
}
#RequestMapping
public Map<String, Object> error(HttpServletRequest aRequest) {
return getErrorAttributes(aRequest, getTraceParameter(aRequest));
}
private boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace");
if (parameter == null) {
return false;
}
return !"false".equals(parameter.toLowerCase());
}
private Map<String, Object> getErrorAttributes(HttpServletRequest aRequest, boolean includeStackTrace)
{
RequestAttributes requestAttributes = new ServletRequestAttributes(aRequest);
return errorAttributes.getErrorAttributes(requestAttributes, includeStackTrace);
} }

Empty Exception Body in Spring MVC Test

I am having trouble while trying to make MockMvc to include the exception message in the response body. I have a controller as follows:
#RequestMapping("/user/new")
public AbstractResponse create(#Valid NewUserParameters params, BindingResult bindingResult) {
if (bindingResult.hasErrors()) throw BadRequestException.of(bindingResult);
// ...
}
where BadRequestException looks sth like this:
#ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "bad request")
public class BadRequestException extends IllegalArgumentException {
public BadRequestException(String cause) { super(cause); }
public static BadRequestException of(BindingResult bindingResult) { /* ... */ }
}
And I run the following test against /user/new controller:
#Test
public void testUserNew() throws Exception {
getMockMvc().perform(post("/user/new")
.param("username", username)
.param("password", password))
.andDo(print())
.andExpect(status().isOk());
}
which prints the following output:
Resolved Exception:
Type = controller.exception.BadRequestException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
MockHttpServletResponse:
Status = 400
Error message = bad request
Headers = {X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY]}
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
Does anybody have an idea on why is Body missing in the print() output?
Edit: I am not using any custom exception handlers and the code works as expected when I run the server. That is, running the application and making the same request to the server returns back
{"timestamp":1423076185822,
"status":400,
"error":"Bad Request",
"exception":"controller.exception.BadRequestException",
"message":"binding failed for field(s): password, username, username",
"path":"/user/new"}
as expected. Hence, there is a problem with the MockMvc I suppose. It somehow misses to capture the message field of the exception, whereas the default exception handler of the regular application server works as expected.
After opening a ticket for the issue, I was told that the error message in the body is taken care of by Spring Boot which configures error mappings at the Servlet container level and since Spring MVC Test runs with a mock Servlet request/response, there is no such error mapping. Further, they recommended me to create at least one #WebIntegrationTest and stick to Spring MVC Test for my controller logic.
Eventually, I decided to go with my own custom exception handler and stick to MockMvc for the rest as before.
#ControllerAdvice
public class CustomExceptionHandler {
#ExceptionHandler(Throwable.class)
public #ResponseBody
ExceptionResponse handle(HttpServletResponse response, Throwable throwable) {
HttpStatus status = Optional
.ofNullable(AnnotationUtils.getAnnotation(throwable.getClass(), ResponseStatus.class))
.map(ResponseStatus::value)
.orElse(HttpStatus.INTERNAL_SERVER_ERROR);
response.setStatus(status.value());
return new ExceptionResponse(throwable.getMessage());
}
}
#Data
public class ExceptionResponse extends AbstractResponse {
private final long timestamp = System.currentTimeMillis();
private final String message;
#JsonCreator
public ExceptionResponse(String message) {
checkNotNull(message, "message == NULL");
this.message = message;
}
}
This likely means that you either didn't handle the exception or you've really left the body empty. To handle the exception either add an error handler in the controller
#ExceptionHandler
public #ResponseBody String handle(BadRequestException e) {
return "I'm the body";
}
or user the global error handler if you're on 3.2 or above
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler
public #ResponseBody String handleBadRequestException(BadRequestException ex) {
return "I'm the body";
}
}
with this the body will be populate, you should populate it with your error message
Updated solution:
If you don't want to do a full integration test but still want to make sure the message is as expected, you can still do the following:
String errorMessage = getMockMvc()
.perform(post("/user/new"))
...
.andReturn().getResolvedException().getMessage();
assertThat(errorMessage, is("This is the error message!");

Passing data in the body of a DELETE request

I have two Spring MVC controller methods. Both receive the same data in the request body (in the format of an HTLM POST form: version=3&name=product1&id=2), but one method handles PUT requests and another DELETE:
#RequestMapping(value = "ajax/products/{id}", method = RequestMethod.PUT)
#ResponseBody
public MyResponse updateProduct(Product product, #PathVariable("id") int productId) {
//...
}
#RequestMapping(value = "ajax/products/{id}", method = RequestMethod.DELETE)
#ResponseBody
public MyResponse updateProduct(Product product, #PathVariable("id") int productId) {
//...
}
In the first method, all fields of the product argument are correctly initialised. In the second, only the id field is initialised. Other fields are null or 0. (id is, probably, initialised because of the id path variable).
I can see that the HttpServletRequest object contains values for all fields in the request body (version=3&name=product1&id=2). They just are not mapped to the fields of the product parameter.
How can I make the second method work?
I also tried to use the #RequestParam annotated parameters. In the method that handles PUT requests, it works. In the DELETE method, I get an exception: org.springframework.web.bind.MissingServletRequestParameterException: Required String parameter 'version' is not present.
I need to pass data in the body of DELETE requests because the data contain a row version which is used for optimistic locking.
The problem is not a Spring problem, but a Tomcat problem.
By default, Tomcat will only parse arguments that are in the form style, when the HTTP method is POST (at least for version 7.0.54 that I checked but it's probably the same for all Tomcat 7 versions).
In order to be able to handle DELETE methods as well you need to set the parseBodyMethods attribute of the Tomcat Connector. The connector configuration is done in server.xml.
Your updated connector would most likely look like:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
parseBodyMethods="POST,PUT,DELETE"
URIEncoding="UTF-8" />
Here is documentation page for configuring Tomcat connectors.
Once you setup Tomcat to parse the parameters, Spring will work just fine (although in your case you will probably need to remove #RequestBody from the controller method)
You can try adding the annotation #RequestBody to your Product argument.
But if you just need to pass version information, using a request param is more appropriate.
So add a new argument in your delete method #RequestParam("version") int version, and when calling the delete method pass a query param like ..ajax/products/123?version=1
As you said request param is not working for you in delete, can you post the exact url you used and the method signature ?
Spring boot 1.5.*
#Bean
public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory(){
#Override
protected void customizeConnector(Connector connector) {
super.customizeConnector(connector);
connector.setParseBodyMethods("POST,PUT,DELETE");
}
};
}
Passing data in the body of a DELETE request
#Component
public class CustomiseTomcat implements WebServerFactoryCustomizer {
#Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers( new TomcatConnectorCustomizer() {
#Override
public void customize(Connector connector) {
connector.setParseBodyMethods("POST,PUT,DELETE");
}
});
}
}
for spring boot 2.0+ :
#Bean
public TomcatServletWebServerFactory containerFactory() {
return new TomcatServletWebServerFactory() {
#Override
protected void customizeConnector(Connector connector) {
super.customizeConnector(connector);
connector.setParseBodyMethods("POST,PUT,DELETE");
}
};
}

Resources