How set RedirectAttributes in Controller Test - java

I need test my controller method
#RequestMapping(path="/add", method = RequestMethod.POST)
public RedirectView addToCart(#ModelAttribute(value="productId") long productId, #ModelAttribute(value="quantity") int quantity, RedirectAttributes redirectAttributes) throws ProductNotFoundException {
RedirectView redirect = new RedirectView("/product/");
redirect.setExposeModelAttributes(false);
try {
redirectAttributes.addFlashAttribute("flash", shoppingCartService.addQuantity(sCart, productId, quantity));
} catch (ExceedsProductQuantityException e) {
e.printStackTrace();
redirectAttributes.addFlashAttribute("flash", new FlashMessage(e.getMessage(), FlashMessage.Status.FAILURE));
}
return redirect;
}
My test code looks like:
#Test(expected = ExceedsProductQuantityException.class)
public void addTooManyToCartTest1() throws Exception {
Product product = productBuilder();
product.setQuantity(15);
Purchase purchase = purchaseBuilder(product); // First purchase
when(productService.findById(1L)).thenReturn(product);
when(sCart.getPurchase()).thenReturn(purchase);
mockMvc.perform(MockMvcRequestBuilders.post("/cart/add")
.param("quantity", String.valueOf(product.getQuantity() + 1))
.param("productId", "1"))
.andExpect(MockMvcResultMatchers.model().attribute("flash", "rdValue"))
.andExpect(MockMvcResultMatchers.flash().attribute("flash", FlashMessage.class));
}
But I get NestedServledException error message, I think its because in my controller method I try to work with RedirectedAttributes, but it's null. So, where and how I have to init and set RedirectedAttributes in my test?

Problem wasnt in RedirectAttributes, there was sCart Mock uninitialized.
I believe you dont need give RedirectAttributes with request as other params.

Related

Spring Controller testing

I am trying to test a post method called saveProduct in my springboot application. It takes params like product, bindingResult, model, multipartFile, redirectAttributes. The product has an attribute photo.
The issue is I am getting an http status is 400 when it is supposed to be 200. I looked at the several online documentation on how to test post api but I still cant figure it out. This is one of the documentation I was looking at on how to test a post multipart uploading springboot application.
https://spring.io/guides/gs/uploading-files/
Controller class (method)
#PostMapping("/showNewProductForm")
public String addAProduct(#Valid #ModelAttribute("saved") Product product,
BindingResult bindingResult,
Model model,
#RequestParam("image") MultipartFile file,
RedirectAttributes redirectAttributes) {
return productService
.saveProduct(product, bindingResult, model, file, redirectAttributes);
}
Service class (method)
public String saveProduct(Product product,
BindingResult bindingResult,
Model model,
MultipartFile file,
RedirectAttributes redirectAttributes) {
Optional<Product> productByName = productRepository.findProductByName(product.getName());
if (bindingResult.hasErrors()) {
return "add-new-product"; // For binding error, you always return to the form instead of the get API
} else if (productByName.isPresent()) {
redirectAttributes.addFlashAttribute("message",
product.getName() + " already exists, please enter a different name");
return "redirect:/showNewProductForm";
}
boolean checkImageUploadStatus = addingImageLogic(product, file);
if (!checkImageUploadStatus) {
redirectAttributes.addFlashAttribute("message", "Please enter an image for " + product.getName());
return "redirect:/showNewProductForm";
}
productRepository.save(product);
model.addAttribute("saved", product);
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded " + product.getName() + "!");
return "redirect:/showNewProductForm";
}
Test class
#WebMvcTest(ProductController.class)
class ProductControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private ProductService productService;
#Test
void addAProduct() throws Exception {
/*
* Product parameters are
* product (name, category, description, photo)
*/
Product product = new Product();
product.setName("Test Run");
product.setCategory("Dairy");
product.setDescription("Lorem 24");
Model model = mock(Model.class);
BindingResult bindingResult = mock(BindingResult.class);
RedirectAttributes redirectAttributes = mock(RedirectAttributes.class);
// When
MockMultipartFile mockMultipartFile = new MockMultipartFile(
"image",
"green-bean.jpg",
"image/png, image/jpg, image/jpeg",
"random img".getBytes()
);
when(productService
.saveProduct(product, bindingResult, model, mockMultipartFile, redirectAttributes))
.thenReturn(String.valueOf(product));
// then
this.mockMvc
.perform(multipart("/showNewProductForm")
.file(mockMultipartFile))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", is("Test Run")));
then(this.productService)
.should()
.saveProduct(product, bindingResult, model, mockMultipartFile, redirectAttributes);
}
}
The error message
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [showNewProductForm], template might not exist or might not be accessible by any of the configured Template Resolvers
Structure
I figured the issue. The error like it says above is my template name is different. It is best practice to name your api the same as your html page. To solve this error, I had to change the name of my html page to showNewProductForm and also the return string to return for binding error in service class.
if (bindingResult.hasErrors()) {
return "add-new-product"; // For binding error, you always return to the form instead of the get API
}
Also there is no need for this (below) in the test method. View the online documentation sample for why it is not needed
when(productService
.saveProduct(product, bindingResult, model, mockMultipartFile, redirectAttributes))
.thenReturn(String.valueOf(product));

Calling RequestMapping "twice"

I couldn't figure out what to put to the title, but I have the following code:
#Controller
public class WorkdayAddController {
#Autowired
private WorkdayRepository workdayRepository;
#Autowired
private VehicleRepository vehicleRepository;
#RequestMapping(value = "addworkday")
public String addWorkday(Model model){
model.addAttribute("workdayaddform", new WorkdayAddForm());
model.addAttribute("vehicles", vehicleRepository.findAll());
return "addworkday";
}
#RequestMapping(value = "saveworkday", method = RequestMethod.POST)
public String save(#Valid #ModelAttribute("workdayaddform") WorkdayAddForm workdayaddform, BindingResult bindingResult) {
if (!bindingResult.hasErrors()) { // validation errors
Date workdayBegin = workdayaddform.getBeginDate();
Date workdayEnd = workdayaddform.getEndDate();
if (!UtilityClass.dateIsAfterDate(workdayBegin, workdayEnd)) {
bindingResult.rejectValue("beginDate", "err.beginDate", "Aloitusaika ei voi olla lopetusajan jälkeen.");
return "addworkday";
}
Workday workday = new Workday();
Vehicle vehicle = new Vehicle();
workdayRepository.save(workday);
}
else {
return "addworkday";
}
return "redirect:/workdaylist";
}
}
After the 'dateIsAfterDate' check, it should direct one to 'addworkday' again, which it does, but it doesn't add the 'vehicles' model. Is there a way around this? I thought it would somehow just direct it to the above #RequestMapping(value= "addworkday") but this seems to not be the case.
Update:
#RequestMapping(value = "addworkday")
public String addWorkday(Model model, RedirectAttributes redirectAttributes){
System.out.println(redirectAttributes); // {}
System.out.println(model); // output in comment
model.addAttribute("workdayaddform", new WorkdayAddForm()); //I guess I need to add the old workdayform here?
model.addAttribute("vehicles", vehicleRepository.findAll());
return "addworkday";
}
#RequestMapping(value = "saveworkday", method = RequestMethod.POST)
public String save(#Valid #ModelAttribute("workdayaddform") WorkdayAddForm workdayaddform,
BindingResult bindingResult,
final RedirectAttributes redirectAttributes) {
if (!bindingResult.hasErrors()) { // validation errors
Date workdayBegin = workdayaddform.getBeginDate();
Date workdayEnd = workdayaddform.getEndDate();
if (!UtilityClass.dateIsAfterDate(workdayBegin, workdayEnd)) {
// Add the vehicle you want to send to the other method.
redirectAttributes.addFlashAttribute("workdayaddform", workdayaddform);
redirectAttributes.addFlashAttribute("vehicle", vehicleRepository.findAll());
redirectAttributes.addFlashAttribute("binding", bindingResult);
return "redirect:/addworkday";
}
You need to use the #RedirectedAttributes annotation in order to send attributes to another method in a controller. Also, you will need to add "redirect:/" to your returned url.
#RequestMapping(value = "saveworkday", method = RequestMethod.POST)
public String save(#Valid #ModelAttribute("workdayaddform") WorkdayAddForm workdayaddform,
BindingResult bindingResult,
final RedirectAttributes redirectAttributes) {
if (!bindingResult.hasErrors()) { // validation errors
Date workdayBegin = workdayaddform.getBeginDate();
Date workdayEnd = workdayaddform.getEndDate();
if (!UtilityClass.dateIsAfterDate(workdayBegin, workdayEnd)) {
// Add the vehicle you want to send to the other method.
redirectAttributes.addFlashAttribute("vehicle", vehicle);
redirectAttributes.addFlashAttribute("binding", bindingResult);
return "redirect:/addworkday";
}
// More code.
else {
redirectAttributes.addFlashAttribute("vehicle", new Vehicle());
return "redirect:/addworkday";
}
}
I wasn't sure if you meant, after the in the else or inside the if, so I add them in both places, just to make sure.

Custom http code with Spring MVC

I use the following code to handle rest calls using Spring MVC.
#RequestMapping(value = "login", method = RequestMethod.GET)
public #ResponseBody
User login(#RequestParam String username, #RequestParam String password) {
User user = userService.login(username, password);
if (user == null)
...
return user;
}
I would like to send the client customer http codes for wrong username, wrong passwords, password changed and password expire conditions. How can I modify the existing code to send these error codes to the client?
You can use controller advice to map exception thrown within controller to some client specific data at runtime.
For example if user is not found, your controller should throw some exception (custom or existed one)
#RequestMapping(value = "login", method = RequestMethod.GET)
#ResponseBody
public User login(#RequestParam String username, #RequestParam String password) {
User user = userService.login(username, password);
if (user == null)
throw new UserNotFoundException(username); //or another exception, it's up to you
return user;
}
}
Then you should add #ControllerAdvice that will catch controller exceptions and make 'exception-to-status' mapping (pros: you will have single point of responsibility for 'exception-to-status-mapping'):
#ControllerAdvice
public class SomeExceptionResolver {
#ExceptionHandler(Exception.class)
public void resolveAndWriteException(Exception exception, HttpServletResponse response) throws IOException {
int status = ...; //you should resolve status here
response.setStatus(status); //provide resolved status to response
//set additional response properties like 'content-type', 'character encoding' etc.
//write additional error message (if needed) to response body
//for example IOUtils.write("some error message", response.getOutputStream());
}
}
Hope this helps.
One way is to add some additional classes for returning HTTP error. Your code will looks like this:
#RequestMapping(value = "login", method = RequestMethod.GET)
#ResponseBody
public User login(#RequestParam String username, #RequestParam String password) {
User user = userService.login(username, password);
if (user == null)
throw new UnauthorizedException();
return user;
}
}
#ResponseStatus(value = HttpStatus.UNAUTHORIZED)
public class UnauthorizedException extends RuntimeException{
}
In this case user will get 401 response status code
I hope it helps
You can return an HTTP 500 or code of your choosing (from the org.springframework.http.HttpStatus enumeration) and use a custom error to emulate something like a SOAP fault within the JSON response.
For example:
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
#ExceptionHandler(YourTargetException.class)
#ResponseBody
Fault caughtYourTargetException(HttpServletRequest req, Exception ex) {
String code = ex.getClass().getName();
String reason = "Caught YourTargetException."
return new Fault(code, reason);
}
The Fault class could look something like this (inspired by http://www.w3.org/TR/soap12-part1/#soapfault):
/**
* A Fault is an object that can be serialized as JSON when expected errors occur.
*/
public class Fault {
#JsonProperty("faultCode")
private final String code;
#JsonProperty("faultReason")
private final String reason;
#JsonProperty("faultDetails")
private final List<String> details = new ArrayList<>();
public Fault(String code, String reason) {
this.code = code;
this.reason = reason;
}
public Fault(String code, String reason, String... detailEntries) {
this.code = code;
this.reason = reason;
details.addAll(Arrays.asList(detailEntries));
}
public String getCode() {
return code;
}
public String getReason() {
return reason;
}
/**
* Zero or more details may be associated with the fault. It carries
* additional information relative to the fault. For example, the Detail
* element information item might contain information about a message
* not containing the proper credentials, a timeout, etc.
* #return Zero or more detail entries.
*/
public Iterable<String> getDetails() {
return details;
}
#Override
public String toString() {
return String.format("Fault %s occurred. The reason is %s.", getCode(),
getReason());
}
}
You could use one of the existing SOAPFaults in Java frameworks, but I have found they don't play well in REST. Creating my own simple version turned out to be simpler.
You can define your own status code and returning objects. In your code throw custom exceptions and then define an exception handler as follows:
#ControllerAdvice
public class GlobalControllerExceptionHandler {
#ExceptionHandler(MyException.class)
public ResponseEntity<MyRetObject> handleControllerError(HttpServletRequest req, MyException ex) {
LOG.warn("My error", ex);
MyRetObject errorMessage = new MyRetObject(ex.getMessage());
return ResponseEntity.status(600).body(errorMessage);
}
}
In your case replace MyExeption.class by UserNotFoundException.class and build your customer error response object and error code

spring mvc #ExceptionHandler method get same view

My problem is that I want to create an #ExceptionHandler method that will capture all un-handled exceptions. Once captured I would like to redirect to the current page instead of specifying a separate page just to display error.
Basically how do I get the value of someview returned by somemethod and set it dynamically in the method unhandledExceptionHandler below.
#ExceptionHandler(Exception.class)
protected ModelAndView unhandledExceptionHandler(Exception ex){
System.out.println("unhandle exception here!!!");
ModelAndView mv = new ModelAndView();
mv.setViewName("currentview");
mv.addObject("UNHANDLED_ERROR", "UNHANDLED ERROR. PLEASE CONTACT SUPPORT. "+ex.getMessage());
return mv;
}
#RequestMapping(value = "/somepage", method = RequestMethod.GET)
public String somemethod(HttpSession session) throws Exception {
String abc = null;
abc.length();
return "someview";
}
So in JSP I can render this error message back into the current page something like that.
<c:if test="${not empty UNHANDLED_ERROR}">
<div class="messageError"> ${UNHANDLED_ERROR}</div>
</c:if>
I don't think there is way to do what you are asking for, because in the exception handler method unhandledExceptionHandler there is no way to find out what the name of the view that the handler method somemethod would have returned.
The only way is for you to introduce some sort of meta data scheme so that when you end up in the exception handler you can figure out what view to map it to. But I think this meta data scheme would be fairly complex. You can implement such a scheme by finding out what was the original url being accessed when the exception was thrown, this can be done with the code snippet below.
(ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()
Once you know what the original request URL you can redirect to it, maybe using flash attribute to store the fact that there was an exception and what the error is.
The main problem wit the metadata will occur when you have a handler method that select between different views something like.
#RequestMapping(value = "/somepage", method = RequestMethod.GET)
public String somemethod(HttpSession session) throws Exception {
String abc = null;
if(someCondition) {
abc.length();
return "someview";
} else {
// do some stuff here.
return "someOtherView";
}
}
Even knowing that somemethod was the source of the error leaves you not knowing which branch in the if statement caused the exception.
I dont think you can do this without modifying all of your handler methods. However you can try to do this in a "pretty" way:
1) You can define your own annotation which will accept target view name as a parameter (e.g. #ExceptionView)
2) Next thing to do is marking your handler methods with it, e.g.:
#ExceptionView("someview")
#RequestMapping(value = "/somepage", method = RequestMethod.GET)
public String somemethod(HttpSession session) throws Exception {
String abc = null;
abc.length();
return "someview";
}
3) After that you can do something like this in exception handler:
#ExceptionHandler(Exception.class)
protected ModelAndView unhandledExceptionHandler(Exception ex, HandlerMethod hm) {
String targetView;
if (hm != null && hm.hasMethodAnnotation(ExceptionView.class)) {
targetView = hm.getMethodAnnotation(ExceptionView.class).getValue();
} else {
targetView = "someRedirectView"; // kind of a fallback
}
ModelAndView mv = new ModelAndView();
mv.setViewName(targetView);
mv.addObject("UNHANDLED_ERROR", "UNHANDLED ERROR. PLEASE CONTACT SUPPORT. "+ex.getMessage());
return mv;
}
Rather than sending the error on a separate page, you can you just put the error in the ModelAndView object. In your case you can just put the try/catch in your controller method and return the same view like so:
#RequestMapping(value = "/somepage", method = RequestMethod.GET)
public String somemethod(ModelAndView mv,HttpSession session) throws Exception {
mv.setViewName("someview");
try{
String abc = null;
abc.length();
} catch(Exception e) {
mv.addObject("UNHANDLED_ERROR", "UNHANDLED ERROR. PLEASE CONTACT SUPPORT. "+ex.getMessage());
}
return mv;
}
So add the ModelAndView to your method and return it.
I have not tried this out, but based on the documentation here, we can get the request object in the exception handler. We may not be able to get the view linked to the URL. Getting the view from the URL, and the state/model of the view will be the tricky part.
#ExceptionHandler(Exception.class)
public ModelAndView handleError(HttpServletRequest req, Exception ex) {
logger.error("Request: " + req.getRequestURL() + " raised " + ex);
ModelAndView mav = new ModelAndView();
mav.addObject("exception", ex);
mav.addObject("url", req.getRequestURL());
mav.setViewName("error");
return mav;
}
Create a controller method annotated with #RequestMethod("/server-error")
Create a controller method annotated with #ExceptionHandler which will return "forward:/server-error";

SpringMVC parameter issue in url

I have a page which i enter parameters to query a list of records.
When the query button is clicked i get the list when an item form the list is clicked it takes me back to the first page with the record in display.
When i hit another button 'new' to clear the page and return an empty page a parameter is there in the url and it sets the value of an item on the page.The item that is there in the url is the crimeRecNo.
How can i get rid of this i want to return an empty page?
Scenario
I am on a page with the following url :http://adomain/crimeTrack/crime_registration.htm
I click Query which does a POST to another url which displays the list of records :http://adomain/crimeTrack/crimeList.htm
On the above page in 2 above i then select one record which does a POST and takes me to the followig url: http://adomain/crimeTrack/getCrime/6.htm - where 6 is the crimeRecNo.
I am now on the above url and i hit the 'NEW' button to get a blank form with the url in 1 above. When i hit new a POST is done to the controller method in code sample 4 under
This method does a redirect to the url which is mapped to a GET method however the final url looks like this : http://adomain/crimeTrack/%20crime_registration.htm?crimeRecNo=6
The value 6 remains in the crimeRecNo field and the entire form is not cleared.
Under is the controller methods:
1. Initial page request
#RequestMapping(value = "crime_registration.htm", method = RequestMethod.GET)
public ModelAndView loadPage(HttpServletRequest request,HttpServletResponse response, #ModelAttribute Crime crime,BindingResult result, ModelMap m, Model model, SessionStatus status,HttpSession session) throws Exception {
try {
logger.debug("In Crime Registration Controller");
myCriminalList.put("dbcriminalList",
this.citizenManager.getListOfCriminals());
...................
session.setAttribute("page", 0);
return new ModelAndView("crime_registration");
} catch (Exception e) {
logger.debug("Exception In CrimeRegistration Controller : "
+ e.getMessage());
return new ModelAndView("crime_registration");
}
}
2. Query For List of Items
#RequestMapping(value = "crimeList.htm", method = RequestMethod.POST)
public ModelAndView handelCrimeList(#ModelAttribute Crime crime,
BindingResult result, ModelMap m, Model model) throws Exception {
if (crimeManager.getCrimesList(crime).size() <= 0) {
model.addAttribute("dbcriminals", myCriminalList);
........
model.addAttribute("crimeTypeList", crimeTypeManager.getCrimeTypeList(crime.getOffenceCatId()));
model.addAttribute("icon", "ui-icon ui-icon-circle-close");
model.addAttribute("results","Error: Query Caused No Records To Be Retrieved!");
return new ModelAndView("crime_registration");
}
model.addAttribute("crimes", crimeManager.getCrimesList(crime));
return new ModelAndView("crimeList");
}
3. Request For One Item/When item is selected from list
#RequestMapping(value = "getCrime/{crimeRecNo}.htm", method = RequestMethod.POST)
public ModelAndView getCrime(#PathVariable Integer crimeRecNo,
#ModelAttribute Crime crime, BindingResult result, ModelMap m,
Model model, HttpServletRequest request,
HttpServletResponse response, HttpSession session) throws Exception {
try {
model.addAttribute("crime", crimeManager.getCrimeRecord(crimeRecNo));
session.setAttribute("crimeRecNo", crimeRecNo);
//model.addAttribute("victimList", citizenManager.getVictimListByCrimeRecNo(crimeRecNo));
} catch (Exception e) {
logger.error("Exception in CitizenRegistrationController - ModelAndView getCitizen "
+ e);
}
int crimeCatId = crimeManager.getCrimeRecord(crimeRecNo).getOffenceCatId();
logger.info("crime category number is : "+crimeCatId);
myCrimeTypeList.put("crimeTypeList", this.crimeTypeManager.getCrimeTypeList(crimeCatId));
model.addAttribute("dbcriminals", myCriminalList);
.....
session.setAttribute("crimeRecNo", crimeRecNo);
return new ModelAndView("crime_registration");
}
4. Request For NEW Form
#RequestMapping(value = "crime_registration_new.htm", method = RequestMethod.POST)
public String loadNew(HttpServletRequest request,Model model,
HttpServletResponse response,SessionStatus status,HttpSession session) throws Exception {
status.setComplete();
return "redirect: crime_registration.htm";
//return new ModelAndView(new RedirectView("crime_registration.htm"));
}
Adding this to 4 did the trick
#RequestMapping(value = "crime_registration_new.htm", method = RequestMethod.POST)
public String loadNew(HttpServletRequest request,Model model,
HttpServletResponse response,SessionStatus status,HttpSession session) throws Exception {
status.setComplete();
model.addAttribute("crime", new Crime());
return "redirect: crime_registration.htm";
//return new ModelAndView(new RedirectView("crime_registration.htm"));
}

Categories