Spring boot patch request on controller with Map parameter. Test with mockmvc - java

I'm trying to test a 'patch request' from my CompanyController that has a Map and Id as a parameters. I expected get a http status 200, but I get a http status 400.
Can someone explain to me what I'm doing wrong? thank you
CompanyController (some parts of the code are omitted):
#RestController
public class CompanyController {
#Autowired
private CompanyService companyService;
#PatchMapping("companies/{id}")
public ResponseEntity<CompanyDTO> patchUpdateCompany(#PathVariable Integer id,
#RequestBody Map<String, Object> updates) throws JsonMappingException {
Optional<CompanyDTO> optionalCompanyDTO = this.companyService.patchUpdateCompany(updates, id);
return ResponseEntity.ok(optionalCompanyDTO.get());
}
}
CompanyControllerTest (some parts of the code are omitted)
#WebMvcTest(CompanyController.class)
public class CompanyControllerTest {
#MockBean
private CompanyService companyService;
#Autowired
private MockMvc mockMvc;
private static List<CompanyDTO> companyDTOList;
#BeforeAll
public static void beforeAll(){
companyDTOList = new ArrayList<>();
CompanyDTO companyDTO1 = CompanyDTO.builder().id(1).name("xavi").build();
CompanyDTO companyDTO2 = CompanyDTO.builder().id(2).name("marteta").build();
companyDTOList.add(companyDTO1);
companyDTOList.add(companyDTO2);
}
#Test
void givenMapAndIdWhenPatchUpdateCompanyThenReturnHttpStatusOk() throws Exception {
Mockito.when(this.companyService.getCompanyById(1)).thenReturn(Optional.of(companyDTOList.get(0)));
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.add("name", "xavi2");
this.mockMvc.perform(patch("/companies/1")
.contentType(MediaType.APPLICATION_JSON)
.params(requestParams))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", Matchers.is("xavi2")));
}
}

Your problem is here
this.mockMvc.perform(patch("/companies/1")
.contentType(MediaType.APPLICATION_JSON)
.params(requestParams)) <---------------------
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", Matchers.is("xavi2")));
}
You pass the data as request parameters. But in your API you have #RequestBody meaning it expects to get the data in http request body and not as request parameters.
This is why you face 400 error meaning Bad Request which is caused by Spring having matched the URL path and also the http method type but something additional which was in the signature of API method was not provided in your request.
So you should use the .content(requestParams) method to set the content you want in the body of the request which you will send
Related documentation
Otherwise your API should have used #RequestParam instead of #RequestBody to receive the input as request parameters as previously sent from test.

Related

MockMVC JsonPath response has empty body?

Testing the controller gives following error: java.lang.AssertionError: No value at JSON path "$.firstName". Apparently the body of my response is empty for some reason. Controller is a RestController and and i'm correctly mocking the service. Can someone help me ?
Testclass:
#WebMvcTest(EmployeeController.class)
class EmployeeControllerTest {
Employee employee = new Employee();
#Autowired
private MockMvc mvc;
#MockBean
private EmployeeServiceImpl service;
#BeforeEach
public void initEmployee() {
employee.setFirstName("John");
employee.setLastName("Doe");
employee.setPlace("xxxx");
employee.setEmployeeTitle(EmployeeTitle.SENIOR_JAVA_DEVELOPER);
employee.setEmployeeId(1L);
}
#Test
public void createEmployeeAPI() throws Exception {
when(service.addNewEmployee(employee)).thenReturn(employee);
mvc.perform(MockMvcRequestBuilders
.post("/employees")
.content(asJsonString(employee))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.firstName").value("John"));
}
Request is correct but apparently the body is of the response is empty (see console output below):
MockHttpServletRequest:
HTTP Method = POST
Request URI = /employees
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Accept:"application/json", Content-Length:"124"]
Body = {"employeeId":1,"firstName":"John","lastName":"Doe","employeeTitle":"SENIOR_JAVA_DEVELOPER","place":"xxxx","photoURrl":null}
Session Attrs = {}
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
Controller code:
#RestController
#RequestMapping("/employees")
public class EmployeeController extends ExceptionHandling {
private final EmployeeService employeeService;
#Autowired
public EmployeeController(EmployeeService employeeService) {
this.employeeService = employeeService;
}
#PostMapping()
public Employee addEmployee(#RequestBody Employee employee) {
return employeeService.addNewEmployee(employee);
}
The problem lies here:
when(service.addNewEmployee(employee)).thenReturn(employee);
Internally Mockito uses an Object class's equals() method to compare object that has been passed to the method as an argument with object configured. If equals() is not overridden then java.lang.Object’s equals() is used which compares only the references, i.e. if both variables point to one and the same object in heap. In the current example, Employe class (probably) has no equals() method implemented. When providing expected a new object is created, references are not one and the same, so Mockito will fail the verification.
Because the employee object is serialized from json, it is not the same object.
You can fix this by using Mockito.any() or Mockito.any(Employee.class).

Java RestTemplate how to POST with body

I need some assistance on the correct way to POST a json body with RestTemplate within a RestController. I am just not familiar enough as to what I am doing wrong. I have spent to much time looking at this and I am not getting the result I need. Hopefully the info I provide is clear. As of right now I am just trying to POST the JSON body to the below URL API
CONTROLLER
#RestController
#RequestMapping("/api/feedback")
public class FeedbackController {
private final RestTemplate restTemplate;
#Autowired
public FeedbackController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
#RequestMapping(
method = RequestMethod.POST
)
public IncidentReport createIncident()
{
return restTemplate.exchange(
"URL_API/create",
HttpMethod.POST, null,
new ParameterizedTypeReference<IncidentReport>(){}
).getBody();
}
JSON POST BODY
{
"group": "my_group",
"short_description":"this is a test for the short description",
"contact_type":"Alert",
}
exchange is the low-level method, and the more specific ones are usually more friendly as long as they cover your use case. You're looking for a postForObject (postForEntity if you need the headers):
return restTemplate.postForObject(url, incidentObject, IncidentReport.class);
StackTrace would have been more helpful. I think you should instantiate RestTemplate in below manner.
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(3000))
.setReadTimeout(Duration.ofMillis(3000))
.build();
}
Then AutoWire it in your Controller class
#Autowired
private RestTemplate myRestTemplate;
Ensure Jackson library is present in the classpath. Try to use myRestTemplate.postForObject(). There is not much to this. If you are still getting an issue try to analyze stack trace, you will get some hint.
Since spring boot autoconfigures RestTemplate and ObjectMapper to serialize/deserialize in your #RestController endpoints you don't have to stick with RestTemplate at all and can use functionality as show in the following code snippet:
#RestController
#RequestMapping("/api/feedback")
public class FeedbackController {
#Autowired private FeedbackFacade feedbackFacade;
#PostMapping("/")
public ResponseEntity<IncidentReport> createIncidentReport(
#RequestBody IncidentReport incidentReport)
{
log.info("Create incident report {}", incidentReport);
var createdIncidentReport = feedbackFacade.create(incidentReport);
log.info("Created incident report: {}", createdIncidentReport);
return new ResponseEntity<>(createdIncidentReport, HttpStatus.CREATED)
}
}

Spring Boot: MockMvc returning empty body for POST request

I'm writing a small demo application with Spring Boot and Spring Data Rest. I have the following model and corresponding repository:
#Entity
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String jobTitle;
public Employee() {
}
... // getters and setters
}
#RepositoryRestResource(collectionResourceRel = "employees", path = "employees")
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Long> {
#RestResource(path = "by-last-name", rel = "by-last-name")
Page<Employee> findByLastNameIgnoreCase(Pageable pageable, #Param("lastName") String lastName);
#RestResource(path = "by-job-title", rel = "by-job-title")
Page<Employee> findByJobTitleIgnoreCase(Pageable pageable, #Param("jobTitle") String jobTitle);
}
If I make the following request through Postman:
POST localhost:8080/employees
{"firstName":"Test","lastName":"McTest","jobTitle":"Tester"}
I receive a full response body with my newly created entity:
{
"firstName": "Test",
"lastName": "McTest",
"jobTitle": "Tester",
"_links": {
"self": {
"href": "http://localhost:8080/employees/120"
},
"employee": {
"href": "http://localhost:8080/employees/120"
}
}
}
However, when I make the same request through my tests as shown below, I get an empty response body:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class)
#AutoConfigureMockMvc
public class EmployeeIntegrationTest {
#Autowired
private MockMvc mvc;
#Autowired
ObjectMapper objectMapper;
#Test
public void testAddEmployee() throws Exception {
Employee employee = new Employee();
employee.setFirstName("Test");
employee.setLastName("McTest");
employee.setJobTitle("Tester");
MockHttpServletRequestBuilder requestBuilder = post("/employees")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(employee));
mvc
.perform(requestBuilder)
.andExpect(status().isCreated())
.andExpect(jsonPath("$.firstName", Matchers.is("Test"))); // Fails, because content is empty.
}
}
For what it's worth, if I then perform a GET /employees in my test, I do in fact see the entity in the response body so I know it's being created.
My expectation is that I would get the same response through either method, alas that's not the case currently, and it seems as though POST requests with MockMvc aren't returning a body. Am I potentially missing a configuration setting somewhere?
Setting the HTTP Accept header in the request should cause a response body to be returned:
MockHttpServletRequestBuilder requestBuilder = post("/employees")
.accept(APPLICATION_JSON)
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(employee));
By default Spring Data REST will only return a response body if the HTTP Accept header is specified. This behavior is documented in the Spring Data REST Reference Guide:
The POST method creates a new entity from the given request body. By default, whether the response contains a body is controlled by the Accept header sent with the request. If one is sent, a response body is created. If not, the response body is empty and a representation of the resource created can be obtained by following link contained in the Location response header. This behavior can be overridden by configuring RepositoryRestConfiguration.setReturnBodyOnCreate(…) accordingly.
My guess is that your Postman request is setting the "Accept" header, which is why you hadn't seen this behavior in Postman.
I was able to solve the issue by explicitly setting
#Override
public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
config.setReturnBodyForPutAndPost(true);
}
inside of a #Configuration class that implements RepositoryRestConfigurer
My guess would be that this is set implicitly for main code, but not for tests.

How to send a query params map using RESTEasy proxy client

I'm looking for a way to pass a map that contains param names and values to a GET Web Target. I am expecting RESTEasy to convert my map to a list of URL query params; however, RESTEasy throws an exception saying Caused by: javax.ws.rs.ProcessingException: RESTEASY004565: A GET request cannot have a body.
. How can I tell RESTEasy to convert this map to a URL query parameters?
This is the proxy interface:
#Path("/")
#Consumes(MediaType.APPLICATION_JSON)
public interface ExampleClient {
#GET
#Path("/example/{name}")
#Produces(MediaType.APPLICATION_JSON)
Object getObject(#PathParam("name") String name, MultivaluedMap<String, String> multiValueMap);
}
This is the usage:
#Controller
public class ExampleController {
#Inject
ExampleClient exampleClient; // injected correctly by spring DI
// this runs inside a spring controller
public String action(String objectName) {
MultivaluedMap<String, String> params = new MultivaluedHashMap<>();
// in the real code I get the params and values from a DB
params.add("foo", "bar")
params.add("jar", "car")
//.. keep adding
exampleClient.getObject(objectName, params); // throws exception
}
}
After hours digging down in RESTEasy source code, I found out that there is no way to do that though interface annotation. In short, RESTEasy creates something called a 'processor' from org.jboss.resteasy.client.jaxrs.internal.proxy.processors.ProcessorFactory to map the annotation to the target URI.
However, it is really simple to solve this issue by creating a ClientRequestFilter that takes the Map from the request body (before executing the request of course), and place them inside the URI query param. Check the code below:
The filter:
#Provider
#Component // because I'm using spring boot
public class GetMessageBodyFilter implements ClientRequestFilter {
#Override
public void filter(ClientRequestContext requestContext) throws IOException {
if (requestContext.getEntity() instanceof Map && requestContext.getMethod().equals(HttpMethod.GET)) {
UriBuilder uriBuilder = UriBuilder.fromUri(requestContext.getUri());
Map allParam = (Map)requestContext.getEntity();
for (Object key : allParam.keySet()) {
uriBuilder.queryParam(key.toString(), allParam.get(key));
}
requestContext.setUri(uriBuilder.build());
requestContext.setEntity(null);
}
}
}
PS: I have used Map instead of MultivaluedMap for simplicity

BindException thrown instead of MethodArgumentNotValidException in REST application

I have a simple Spring Rest Controller with some validation. My understanding is that validation failures would throw a MethodArgumentNotValidException. However, my code throws a BindException instead. In debug messages, I also see the app returning a null ModelAndView.
Why would a Rest Controller throw BindException or return a null ModelAndView?
Note: I am testing my web application using curl and making an HTTP POST
curl -X POST http://localhost:8080/tasks
I am intentionally omitting the "name" parameter which is a required field marked with #NotNull and #NotBlank annotations.
My Controller:
#RestController
public class TasksController {
private static Logger logger = Logger.getLogger(TasksController.class);
#Autowired
private MessageSource messageSource;
#Autowired
private Validator validator;
#InitBinder
protected void initBinder(WebDataBinder binder){
binder.setValidator(this.validator);
}
#RequestMapping(value = "/tasks", method = RequestMethod.POST)
public Task createTask(#Valid TasksCommand tasksCommand){
Task task = new Task();
task.setName(tasksCommand.getName());
task.setDue(tasksCommand.getDue());
task.setCategory(tasksCommand.getCategory());
return task;
}
}
My "command" class (that contains validation annotations)
public class TasksCommand {
#NotBlank
#NotNull
private String name;
private Calendar due;
private String category;
... getters & setters ommitted ...
}
My RestErrorHandler class:
#ControllerAdvice
public class RestErrorHandler {
private static Logger logger = Logger.getLogger(RestErrorHandler.class);
#Autowired
private MessageSource messageSource;
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
public ErrorsList processErrors(MethodArgumentNotValidException ex){
logger.info("error handler invoked ...");
BindingResult result = ex.getBindingResult();
List<FieldError> fieldErrorList = result.getFieldErrors();
ErrorsList errorsList = new ErrorsList();
for(FieldError fieldError: fieldErrorList){
Locale currentLocale = LocaleContextHolder.getLocale();
String errorMessage = messageSource.getMessage(fieldError, currentLocale);
logger.info("adding error message - " + errorMessage + " - to errorsList");
errorsList.addFieldError(fieldError.getField(), errorMessage);
}
return errorsList;
}
}
The processErrors method marked with #ExceptionHandler(...) annotation never gets called. If I try to catch a BindException using #ExceptionHandler(...) annotation, that handler method does get invoked.
I have couple of support classes - Task, TaskCommand, Error and ErrorsList - that I can post code for if needed.
The problem was with my curl command.
curl -d sends the Content-Type "application/x-www-form-urlencoded". As a result, Spring interprets the data as web form data (instead of JSON). Spring uses FormHttpMessageConverter to convert body of POST into domain object and results in a BindException.
What we want is for Spring to treat POST data as JSON and use the MappingJackson2HttpMessageConverter to parse body of POST into object. This can be done by specifying the "Content-Type" header with curl command:
curl -X POST -H "Content-Type: application/json" -d '{"name":"name1", "due":"2014-DEC-31 01:00:00 PDT", "category":"demo"}' http://localhost:8080/tasks
See this post for how to post JSON data using curl:
How to POST JSON data with Curl from Terminal/Commandline to Test Spring REST?
Also, here's the relevant Spring documentation about MessageConverters:
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-requestbody
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/remoting.html#rest-message-conversion

Categories

Resources