i have a common problem to test Post-Request with Bean Validation.
Requirements: ContentType is APPLICATION_FORM_URLENCODED and NOT JSON
Model under test:
public class Message extends Auditable{
#Id
private long id;
private String messageText;
#NotNull
private Link link;
}
How it works on browser properly:
I'am just submit data. On Browser Dev-Tools, i see, that browser sends
only to fields: messageText="my message" and link="1"
problem: during MockMVC Post-Request, i can not convert param-value "1" to the object Link.
this.mockMvc.perform(MockMvcRequestBuilders.post("/links/link/comments")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("link", "1")
.param("messageText", "hello message"))
.andDo(print())
.andExpect(status().is3xxRedirection());
}
My post-handler on controller
#PostMapping(value = "/link/message")
public String saveNewComment(#Valid Message message, BindingResult bindingResult, RedirectAttributes attributes) {
if(bindingResult.hasErrors()) {
model.addAttribute("newMessage", message);
return "link/submit";
}
}
BindingResult complains about TypeMismatch from "String" to "Link".
How can i pass some Marschal- or Converter-Object, which enables BindingResult to convert string-value to appropriate object?
I don't want to implement on server-side own validator-object (which implements validator interface), cause on production it works properly without any additional code.
I've solved this issue by myself with small fix. Nested objects should parameterize with explizit field-name. Instead of param("link", "1") it must be param("link.id", "1")
Related
Having this handler method:
#RequestMapping(method = RequestMethod.POST, value = "/myform1")
public String formPost(#ModelAttribute("myModel") #Valid MyModel myModel, BindingResult binder)
how can I ignore errors on certain (or all) fields?
Even if I omit the #Valid annotation, it still detects errors like "abc entered into a Number field" (ie binder.hasErrors() returns true). And the the error message (from the catalog) is displayed on the final web page, which I do not want.
If I omit the BindingResult binder, then the code never reaches this method but throws an exception/error before it.
Is there a #DontValidateAtAll annotation or some other method to achieve this?
Related problem: I can not override the bad value in the method, it keeps showing the old rejected value. For example, user enters "abc" into myModel.someNumber and submits the form, then even if I do myModel.setSomeNumber(22) in the method formPost(), after returning from it, the web page (JSP) will show "abc" (and the error text) instead of 22 in that field.
For the specific String-to-Number conversion exception you were referring to, you can use the below manipulation. This is a type conversion exception that occurs even before Spring MVC's form validation, ie, even before the validate() method.
If your only purpose is to NOT see the errors on your final web page, then you can write extra code in your Controller method.
#RequestMapping(method = RequestMethod.POST, value = "/myform1")
public ModelAndView formPost(#ModelAttribute("myModel") #Valid MyModel myModel, BindingResult binder){
List<ObjectError> errors = bindingResult.getAllErrors();
boolean hasIgnorableErrorsOnly = true;
if(bindingResult.hasErrors()){
for(ObjectError error : errors){
if(error.getCode().equals("myModel.someNumber.NotANumber")){ //or whatever your error code is
}else{
hasIgnorableErrorsOnly = false;
}
}
}
if(hasIgnorableErrorsOnly){
//You have not shown where your final view is. I am assuming your myModel form view is myform.jsp and final web page is myModel.jsp
// Notice that I have also changed this method signature to return ModelAndView instead of only String view.
if(myModel.getSomeNumber() == null)
myModel.setSomeNumber(22);
return new ModelAndView("myModel.jsp", myModel); //Take care of view Resolvers here
}else{
return new ModelAndView("myform.jsp", myModel); //Take care of view Resolvers here
}
}
Now, if your BindingResult has more than ignorable errors, then it would go to myModel.jsp and I believe you already have code in place for display of errors. But if, because of above code, you are forwarded to myModel.jsp, you would have to iterate over the ${errors} key on your jsp and write the someNumber element so that it does not show errors. For example,
<spring:hasBindErrors name="myModel">
<c:forEach items="${errors.allErrors}" var="error">
<c:if test="${error.code eq 'myModel.someNumber.NotANumber'}">
//skip your display of global errors
</c:if>
</c:forEach>
</spring:hasBindErrors>
This works:
add a (Model)Map parameter to the handler method (it is usually used anyway, I omitted it in the question for brevity)
overwrite the model attribute with a fresh copy
Code:
#RequestMapping(method = RequestMethod.POST, value = "/myform1")
public String formPost(#ModelAttribute("myModel") #Valid MyModel myModel, BindingResult binder, Map<String, Object> modmap) {
if(ignore_errors) {
modmap.put("myModel", new MyModel());
return "myForm.jsp";
} // else ... other things
}
Apparently this procedure makes the framework to "forget" about the validation errors.
Note: I use Spring 3.0.x, other versions might behave differently.
I am trying to test my controller endpoint and my requestbody annotated with #Valid annotation. My Testclass looks like the follow:
#RunWith(SpringRunner.class)
#WebMvcTest(value = BalanceInquiryController.class, secure = false)
public class BalanceInquiryControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private BalanceInquiryController balanceInquiryController;
#Test
public void testGetBalanceInquiry() throws Exception {
RequestBuilder requestBuilder = MockMvcRequestBuilders
.post("/com/balanceInquiry")
.accept(MediaType.APPLICATION_JSON)
.content("{\"comGiftCard\":{\"cardNumber\":\"1234567890\",\"pinNumber\":\"0123\"},\"comMerchant\":\"MERCHANT1\"}")
.contentType(MediaType.APPLICATION_JSON);
MvcResult mvcResult = mockMvc.perform(requestBuilder).andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
assertEquals(HttpStatus.OK.value(), response.getStatus());
}
}
My Controller - #PostMapping looks like that:
#PostMapping(value = "/com/balanceInquiry")
public ResponseEntity<?> getBalanceInquiry(#Valid #RequestBody BalanceInquiryModel balanceInquiry, Errors errors) {
if (errors.hasErrors()) {
return new ResponseEntity<String>("Validation error", HttpStatus.BAD_REQUEST);
}
//do any stuff...
return new ResponseEntity<BalanceInquiryResponse>(balanceInquiryResponse, HttpStatus.OK);
}
My BalanceInquiryModel is annotated with #Valid and has some hibernate and custom validations behind. Those validations are all ok and already unit tested.
What I like to test is my endpoint where I send a valid json request body expecting a 200 response and also an invalid json request body expecting a 400 response validated by the set #Valid implementation.
For example an unvalid call is to send no pinNumber or length < 4.
I have read some threads and some uses MockMvcBuilders.standaloneSetup() to mock the full controller. But I wont do a full integration test.
Not quite sure how to go on with this situation and if I should go on.
P.S.: At the moment I get always a 200 response no matter if the validation should give an error or not.
Here a gist for more code and the validation classes/models.
Here's one of my example I work on my project
hope it help you out:
I have a global exception handler to handler my MethodArgumentNotValidException and throw it
#RequestMapping(value = "/add", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<User> createUser(#Valid #RequestBody User user) {
User savedUser = userService.save(user);
return new ResponseEntity<User>(savedUser, HttpStatus.CREATED);
}
public void testAdduser() throws Exception{
final User request = new User();
request.setFirstName("Test");
request.setLastName("some description");
mockMvc.perform(post(END_POINT+"/add")
.contentType(MediaType.APPLICATION_JSON)
.content(stringify(request))
).andDo(print()).andExpect(status().isUnprocessableEntity())
;
}
private String stringify(Object object) throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(object);
}
Update:
I think your main problem is that you are using #WebMvcTest in stead of #SpringBootTest.
the different between 2 of them is that:
#SpringBootTest annotation will loads complete application and injects all the beans which is can be slow.
#WebMvcTest - for testing the controller layer. it doesn't inject other bean beside the #RestController
so if you are just testing just pure controller to see u can reach the endpont then you can just use #WebMvcTest which will make your test run faster.
but in your case, you want it to run the spring validation, you will need to use #SpringBootTest
for detailed: https://spring.io/guides/gs/testing-web/
I have this Object
public class Deportista implements Serializable {
private static final long serialVersionUID = 6229604242306465153L;
private String id;
...
#NotNull(message="{field.null}")
public String getId() {
return id;
}
...
}
I have the following Controller's methods
#InitBinder(value="deportistaRegistrar")
public void registrarInitBinder(WebDataBinder binder) {
logger.info(">>>>>>>> registrarInitBinder >>>>>>>>>>>>>");
}
#RequestMapping(value="/registrar.htm", method=RequestMethod.GET)
public String crearRegistrarFormulario(Model model){
logger.info("crearRegistrarFormulario GET");
Deportista deportista = new Deportista();
model.addAttribute("deportistaRegistrar", deportista);
return "deportista.formulario.registro";
}
#RequestMapping(value="/registrar.htm", method=RequestMethod.POST)
public String registrarPerson(#Validated #ModelAttribute("deportistaRegistrar") Deportista deportista,
BindingResult result){
logger.info("registrarPerson POST");
logger.info("{}", deportista.toString());
if(result.hasErrors()){
logger.error("There are errors!!!!");
for(ObjectError objectError : result.getAllErrors()){
logger.error("Error {}", objectError);
}
return "deportista.formulario.registro";
}
logger.info("All fine!!!!");
this.fakeMultipleRepository.insertDeportista(deportista);
return "redirect:/manolo.htm";
}
Until here the Controller is able to create a form (GET) and submit (POST) a new command object, Validation code works well.
The problem is with the update.
I have the following:
#InitBinder(value="deportistaActualizar")
public void actualizarInitBinder(WebDataBinder binder) {
logger.info(">>>>>>>> actualizarInitBinder >>>>>>>>>>>>>");
binder.setDisallowedFields("id");
}
Observe I have binder.setDisallowedFields("id")
public String crearActualizarFormulario(#PathVariable("id") String id, Model model){
logger.info("crearActualizarFormulario GET");
Deportista deportista = this.fakeMultipleRepository.findDeportista(id);
model.addAttribute("deportistaActualizar", deportista);
return "deportista.formulario.actualizacion";
}
#RequestMapping(value="/{id}/actualizar.htm", method=RequestMethod.POST)
public String actualizarPerson(#Validated #ModelAttribute("deportistaActualizar") Deportista deportista,
BindingResult result){
logger.info("actualizarPerson POST");
logger.info("{}", deportista.toString());
if(result.hasErrors()){
logger.error("There are errors!!!!");
for(ObjectError objectError : result.getAllErrors()){
logger.error("Error {}", objectError);
}
return "deportista.formulario.actualizacion";
}
logger.info("All fine!!!!");
this.fakeMultipleRepository.updateDeportista(deportista);
return "redirect:/manolo.htm";
}
The problem is:
when the form or command has any error, the controller re-render the view and the form appear showing the error messages how is expected, but without the ID value
or
if I try to update the object, of course keeping the id value, and without any error to simply proceed to update, it fails
The following appears in the Console:
- -------- createCollections ---------------
- >>>>>>>> actualizarInitBinder >>>>>>>>>>>>>
- Skipping URI variable 'id' since the request contains a bind value with the same name.
- actualizarPerson POST
- Deportista [id=null, nombre=Manuel, ...]
- There are errors!!!!
- Error Field error in object 'deportistaActualizar' on field 'id': rejected value [null]; codes [NotNull.deportistaActualizar.id,NotNull.id,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [deportistaActualizar.id,id]; arguments []; default message [id]]; default message [The field must be not empty]
The id is null. How I can around this problem keeping the Request Scope?
I have an alternate controller which is working with #SessionAttributes and all works perfect. But since is a huge risk if the user has many tabs open in the same web browser, one for create and other for updating, all is going to be very wrong. According with Spring MVC + Session attributes and multiple tabs, request scope should be used instead of session scope. It has sense.
Sadly seems Spring is not going to fix this:
#SessionAttributes doesn't work with tabbed browsing
Addition
According with your suggestion, I have the following:
#ModelAttribute("deportistaActualizar")
public Deportista populateActualizarFormulario(#RequestParam(defaultValue="") String id){
logger.info("populateActualizarFormulario - id: {}", id);
if(id.equals(""))
return null;
else
return this.fakeMultipleRepository.findDeportista(id);
}
Observe the method uses #RequestParam, my problem is how update that method to work when the URL to update has the following style
http://localhost:8080/spring-utility/deportista/1/actualizar.htm. There is no param in the URL, therefore #RequestParam is useless now.
I already have read the Spring Reference documentation:
Using #ModelAttribute on a method
Second Addition
Yes, you was right, and I did that yesterday, but I forget to share the following:
#ModelAttribute("deportistaActualizar")
public Deportista populateActualizarFormulario(#PathVariable(value="id") String id){
logger.info("populateActualizarFormulario - id: {}", id);
if(id.equals(""))
return null;
else
return this.fakeMultipleRepository.findDeportista(id);
}
Since a #ModelAttribute is called always before by any handler method, the following URL fails http://localhost:8080/spring-utility/deportista/registrar.htm, the following appears on the page
HTTP Status 400 -
type Status report
message
description The request sent by the client was syntactically incorrect.
Of course because the URL does not contains the expected id. Therefore I can't create new records to later edit/see.
I can confirm, that for the following work:
http://localhost:8080/spring-utility/deportista/1/detalle.htm
http://localhost:8080/spring-utility/deportista/1/actualizar.htm
the id (1) is retrieved.
How I could resolve this?
Thank You
I have a problem with a validation error displayed when i submit my form with an empty date like that in the resulting web page:
Failed to convert property value of type java.lang.String to required type
java.util.Date for property dateFin; nested exception is
java.lang.IllegalArgumentException: Could not parse date: Unparseable date: ""
My controller looks like this:
#Controller
#SessionAttributes
#Lazy
public class MyController extends AbstractMVPAction {
#RequestMapping(value = "/secured/cp/saveProgram")
public String enregistrerProgramme(#ModelAttribute Program program,
BindingResult bindingResult, ModelMap model){
if(bindingResult.hasErrors()){
model.put("program", program);
return "/secured/cp/showProgram"
}else{
// ... saves the programme
model.put("program", null);
return "/secured/cp/backToOtherPage"
}
}
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Date.class, new CustomDateEditor(
new SimpleDateFormat("dd/MM/yyyy"), false));
}
}
When I debug my method, I can see my object is fine, the modif I did are well reported, the date is null, but the bindingResult.hasErrors() returns true and according to me it shouldn't.
I used to have some validation annotations in the Program object and a #Valid annotation but I removed them all and still have the problem.
I have read lot's of similar issues and every time the solution is the #InitBinder/CustomDateEditor.
So it is there and I guess it's working, the dates are displayed the way I want (this was not the case before I add it) and I can submit them provided it's not empty.
Thank's in advance, I'm starting to go crazy...
You've constructed a CustomDateEditor that explicitly disallows the empty string. Check the Javadoc for the constructor you're using, when the boolean argument is false, passing an empty string to that editor causes the IllegalArgumentException you're seeing.
Try this:
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Date.class, new CustomDateEditor(
new SimpleDateFormat("dd/MM/yyyy"), false));
}
I have the following method skeleton in a Spring MVC application:
#RequestMapping(value = "/activateMember/{token}", method = RequestMethod.GET, produces = "text/html")
public String activateMember(#PathVariable("token") String token) {
...
}
I am trying to display an error message if the token is invalid for some reason. However I have no ModelAttribute in the method arguments and I don't really want one. But of course I can't use an Errors or BindingResults argument because of the absence of a ModelAttribute and its corresponding form.
So my question is:
what is the recommended way to display an error message given the above method signature and without introducing a ModelAttribute?
If the String you've returned from the method is a viewname (Spring default) then simply create a view for this case and do like:
#RequestMapping()
public String activateMember(#PathVariable("token") String token) {
if(checkToken(token)){
doProcess();
return "userprofile";
} else {
return "badtoken"
}
}
In more complicated case you may have a hierarchy of exceptions, related to bad tokens. (Token is expired, token is just incorrect and so on). You can register an #ExceptionHandler in the same controller:
#RequestMapping()
public String activateMember(#PathVariable("token") String token) {
return activate(token); // This method may throw TokenException and subclasses.
}
#ExceptionHandler(TokenException.class)
public ModelAndView tokenException(TokenException e){
// some code
return new ModelAndView("badtoken", "exception", e);
}