I'm working on a spring boot CRUD RESTful API with an User entity that consists of two parameters : name and id. My test framework is JUnit.
The problem i'm facing is that i don't know how to treat a throwable UserNotFound exception on my services unit tests.
I have possible "User not found by {id}" exceptions in my "List user by id", "Delete user by id" and "Update user by id" as you can see (i'll only list two endpoints to make this shorter) :
#Service
public class DeleteUserService {
#Autowired
UserRepository repository;
public void deleteUser(Long id) {
Optional<User> userOptional = repository.findById(id);
if (!userOptional.isPresent()) {
throw new UserNotFoundException(id);
} else {
repository.deleteById(id);
}
}
}
#Service
public class DetailUserService {
#Autowired
UserRepository repository;
public Optional<User> listUser(Long id) {
Optional<User> user = repository.findById(id);
if (!user.isPresent()) {
throw new UserNotFoundException(id);
} else {
return repository.findById(id);
}
}
}
Nothing wrong so far, my endpoints are working fine.
The UserNotFound code is :
#ControllerAdvice
public class UserNotFoundAdvice {
#ResponseBody
#ExceptionHandler(UserNotFoundException.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
String userNotFoundHandler(UserNotFoundException ex) {
return ex.getMessage();
}
}
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("Could not find user with id " + id + ".");
}
}
The unit tests (the main reason this is being written) :
#RunWith(MockitoJUnitRunner.class)
public class DeleteUserServiceTest {
#Mock
private UserRepository userRepository;
#InjectMocks
private DeleteUserService deleteUserService;
#Test
public void whenGivenId_shouldDeleteUser_ifFound(){
User user = new User();
user.setId(89L);
deleteUserService.deleteUser(user.getId());
verify(userRepository).deleteById(user.getId());
}
}
#RunWith(MockitoJUnitRunner.class)
public class DetailUserServiceTest {
#Mock
private UserRepository userRepository;
#InjectMocks
private DetailUserService detailUserService;
#Test
public void whenGivenId_shouldReturnUser_ifFound() {
User user = new User();
user.setId(89L);
Optional<User> userMock = Optional.of(user);
when(userRepository.findById(user.getId())).thenReturn(userMock);
Optional<User> expected = detailUserService.listUser(user.getId());
assertThat(expected).isSameAs(userMock);
verify(userRepository).findById(user.getId());
}
}
As you can see, there's something missing in these unit tests code which is the behavior of the UserNotFound. Perhaps it is not properly mocked or something else's missing in the unit tests code??
Would really appreciate if someone could help me with this one! Sorry if the post's too long, i tried my best to explain it!
If I understand you right you need to test the behavior when the user is not found and you throw an exception.
Here is the link about how to test exception: https://www.baeldung.com/junit-assert-exception
And also additionally you can verify that delete by id or find by id weren't invoked:
verify(userRepository, never()).findById(user.getId());
or
verify(userRepository, Mockito.times(0)).findById(user.getId());
and for the deleteById the same
To test that exception handlers were invoked and worked correctly you need integration tests.
Related
I'm currently unit testing the endpoints of a Spring Boot CRUD RESTful API and i have the following "Update user by its id" endpoint that's composed of a controller and a service to implement its logic :
Update controller (it's mainly calling the logic of the service and defining some guidelines) :
#RestController
#RequestMapping("/users/{id}")
public class UpdateUserController {
#Autowired
UpdateUserService service;
#PutMapping
#ResponseStatus(HttpStatus.OK)
public ResponseEntity<User> updateUser_whenPutUser(#RequestBody User user, #PathVariable Long id) {
return ResponseEntity.ok().body(service.updateUser(id, user));
}
}
Update service :
#Service
public class UpdateUserService {
#Autowired
UserRepository repository;
public User updateUser(Long id, User user) {
repository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
user.setId(id);
return repository.save(user);
}
}
While everything seems to be working fine until now, i'd appreciate if someone could tell me what i could improve in the code above. Anyways, my controllers throw exception unit test is the problem i can't seem to solve :
#RunWith(SpringRunner.class)
#WebMvcTest(UpdateUserController.class)
public class UpdateUserControllerTest {
#Autowired
private MockMvc mvc;
#MockBean
private UpdateUserService updateUserService;
#Test
public void updateUser_whenPutUser() throws Exception {
User user = new User();
user.setName("Test Name");
user.setId(89L);
given(updateUserService.updateUser(user.getId(), user)).willReturn(user);
ObjectMapper mapper = new ObjectMapper();
mvc.perform(put("/users/" + user.getId().toString())
.content(mapper.writeValueAsString(user))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("name", is(user.getName())));
}
#Test
public void should_throw_exception_when_user_doesnt_exist() throws Exception {
User user = new User();
user.setId(89L);
user.setName("Test Name");
Mockito.doThrow(new UserNotFoundException(user.getId())).when(updateUserService).updateUser(user.getId(), user);
mvc.perform(put("/users/" + user.getId().toString())
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
}
Although the updateUser_whenPutUser is passing, the exceptions unit test is resulting in :
java.lang.AssertionError: Status expected:<404> but was:<400>
Expected :404
Actual :400
Which means it's getting a 400 response status instead of the expected 404 thrown by the exception.
Curiously, if i change my #WebMvcTest(UpdateUserController.class) to #WebMvcTest(UpdateUserService.class) it passes, but then my controllers main unit test fails. No idea why.
Would appreciate if someone could help with this one.
First you can give your Test Annotation an expected type. Like this
#Test(expected = NotFoundException.class)
public void test(){
...
}
The reason why you get an 400 instead of an 404 is hard to say. I would suggest it has something to do with the status() methode. Maybe you can provide some more of the stacktrace? Or i think you need to debug your tests to find out where the exception happens.
I've found the solution :
The reason it was returning a 400 response status error is because no message body was being sent.
This specific endpoint requires a message body written in JSON on the request :
#Test
public void should_throw_exception_when_user_doesnt_exist() throws Exception {
User user = new User();
user.setId(89L);
user.setName("Test Name");
Mockito.doThrow(new UserNotFoundException(user.getId())).when(updateUserService).updateUser(user.getId(), user);
ObjectMapper mapper = new ObjectMapper();
mvc.perform(put("/users/" + user.getId().toString())
.content(mapper.writeValueAsString(user))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
There is a controller whose task is to return the user profile to me using the REST API. Code further:
#PostMapping("/me")
public UserProfileResponse getUserProfile(#AuthenticationPrincipal UserAuthenticationPrincipalModel user ) {
return userProfileService.getUserProfile(user.getUserId());
}
I created a model for the User entity. The entity class is created as:
public class User implements UserDetails { ... }
The model has the following structure:
public class UserAuthenticationPrincipalModel extends User {
private String userId;
private String avatarUrl;
public UserAuthenticationPrincipalModel(***.********.entity.User user) {
super(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
this.userId = user.getUserId();
this.avatarUrl = user.getUserPic();
}
// + equals and hashCode
}
In the model, the data that I will ever (or so far plan so) to pull from the AuthPrincipal an authorized user. According to the statement of work, I can’t use the default Principal, I haven’t even tried it. Implementation of UserDetailsService:
#Service
public class UserDetailsServiceImpl extends AbstractMySQLService<User, String, UserRepository> implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(final UserRepository userRepository, final UserRepository repository) {
super(repository);
this.userRepository = userRepository;
}
#Override
public UserAuthenticationPrincipalModel loadUserByUsername(final String email) {
User user = userRepository.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException("Invalid username or user not e: " + email);
}
return new UserAuthenticationPrincipalModel(user);
}
}
Error: Null always flies into methods. Made a lot of additions, which are recommended on the Baeldang and this stack - nothing :(
Please, write a comment, if I should add some more information.
UPD 1:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/ca/**").hasRole("COMPANY_ADMIN")
.antMatchers("/d/**").hasRole("DRIVER")
.antMatchers("/u/**").authenticated()
.antMatchers("/sign_up", "/oauth/token", "/swagger-ui.html", "/resources").permitAll();
}
I can give you a few pointers of how to approach this issue.
Ensure you are using org.springframework.security.core.annotation.AuthenticationPrincipal instead of #org.springframework.security.web.bind.annotation.AuthenticationPrincipal (Both should work but just pre-caution because the later is deprecated)
Now the issue is to isolate the problem to ONE of the following areas so you can concentrate there:
Your UserDetailsServiceImpl is not used
Something wrong with getUserProfile method with #AuthenticationPrincipal
user is not associated with a logged in session.
To identify that, replace your public UserProfileResponse getUserProfile method with the following:
[Do not change anything else]
#Autowired
private UserDetailsService userDetailsService;
#PostMapping("/me")
public void getUserProfile(#AuthenticationPrincipal UserDetails user ) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println("principal : " + authentication.getPrincipal());
System.out.println("Implementing class of UserDetails: " + authentication.getPrincipal().getClass());
System.out.println("Implementing class of UserDetailsService: " + userDetailsService.getClass());
}
Check the logs and it will tell you where the problem is and if you can't figure out from it, you can post the outcome here for more help
Update: Answers for point 4 given as below in comments.
principal : anonymousUser
Implementing class of UserDetails : class java.lang.String
Implementing class of UserDetailsService : class
Conclusion : endpoint is not protected and user accessing without logging in
Solution : Protect the endpoint by replacing .antMatchers("/u/**").authenticated() with .antMatchers("/api/u/**").authenticated()
I'm working on a Spring Boot CRUD RESTful API and i'm trying to define the best way of doing certain things, for instance :
This is my List user by its id endpoint service :
#Service
public class DetailUserService {
#Autowired
UserRepository repository;
public Optional<User> listUser(Long id) {
Optional<User> user = repository.findById(id);
if (!user.isPresent()) {
throw new UserNotFoundException(id);
} else {
return repository.findById(id);
}
}
}
And this is another way of writing it :
#Service
public class DetailUserService {
#Autowired
UserRepository repository;
public User listUser(Long id) {
return repository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
}
Both ways work but how do i know which is better?
Using java-8 is always a better choice for less code and more readable code.
You can use below tyle of as you mentioned it as your second option.
Using the Optional.orElseThrow() method represents another elegant alternative to the isPresent()-get() pair
You can find out more here
https://dzone.com/articles/using-optional-correctly-is-not-optional
#Service
public class DetailUserService {
#Autowired
UserRepository repository;
public User listUser(Long id) {
return repository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
}
I am trying to create a website that allows the user to update, edit, delete, etc., and I have got to the part of Updating or Editing user’s information. I have tried multiple times using different ways, but I cannot seem to get past the error. I am completely oblivious to Optional<> I just don’t get it. I have read https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html, but i dont understand how it should be coded, its just not clicking. If someone could please inform on how it should be coded in my code and please explain it I would be so grateful. Maybe its because im overworked and have not slept, but i cannot seem to correct this error. This is the error i get on the page when i attempt to edit the information for a user:
There was an unexpected error (type=Internal Server Error, status=500).
For input string: "id"
java.lang.NumberFormatException: For input string: id
//Repository
public interface UserRepository extends CrudRepository<User, Integer> {
}
here is the UserService
//UserService
#Service
#Transactional
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository=userRepository;
}
public void saveMyUser(User user) {
userRepository.save(user);
}
public List<User> showAllUsers(){
List<User> users = new ArrayList<User>();
for(User user: userRepository.findAll()) {
users.add(user);
}
return users;
}
public void deleteMyUser(int id) {
userRepository.deleteById(id);
}
public User editUser (int id) {
return userRepository.findById(id);//I also get an error here as well
}
}
here is the controller
//Application Controller
#Controller
public class ApplicationController {
#Autowired
private UserService userService;
// THIS IS WHERE I GET THE ERROR
#RequestMapping("/edit-user")
public String editUser(#RequestParam int id,HttpServletRequest request) {
/* OPTIONAL<?> is this where i would implement the
optional what do i have to put here exactly?
I tried some ways I read about but its not working for me */
request.setAttribute("user", userService.editUser(id));
request.setAttribute("mode", "MODE_UPDATE");
return "welcome";
}
}
Thank you for the help in advance Im a little frustrated with this because I have been trying to correct this error all night.
There are several ways to convert from an option to an entity. You can use the following:
Use get() method:
public User editUser (int id) {
return userRepository.findById(id).get();
}
Use orElse method:
public User editUser (int id) {
/* new User() is stab if user was not found */
return userRepository.findById(id).orElse(new User());
}
Use orElseThrowMethod:
public User editUser (int id) {
/* Throw exception if user was not found*/
return userRepository.findById(id).orElseThrow(IllegalArgumentException::new));
}
As for controller it will be like this:
#RequestMapping("/edit-user")
public String editUser(#RequestParam int id,HttpServletRequest request) {
User user = userService.editUser(id);
request.setAttribute("user", user);
request.setAttribute("mode", "MODE_UPDATE");
return "welcome";
}
Also there similar question for your topic:
Spring Boot. how to Pass Optional<> to an Entity Class
I have read a lot of #Transactional annotation, I saw stackoverflow answers but it does not help me. So I am creating my question.
My case is to save user with unique email. In DB I have user with email xxx#xxx.com, and I am saving user with the same email address. For saving I have to use entityManager.merge() because of this post thymeleaf binding collections it is not important.
First example:
#Controller
public class EmployeeController extends AbstractCrudController {
// rest of code (...)
#RequestMapping(value = urlFragment + "/create", method = RequestMethod.POST)
public String processNewEmployee(Model model, #ModelAttribute("employee") User employee, BindingResult result, HttpServletRequest request) {
prepareUserForm(model);
if (!result.hasErrors()) {
try {
saveEmployee(employee);
model.addAttribute("success", true);
} catch (Exception e) {
model.addAttribute("error", true);
}
}
return "crud/employee/create";
}
#Transactional
public void saveEmployee(User employee) {
entityManager.merge(employee);
}
private void prepareUserForm(Model model) {
HashSet<Position> positions = new HashSet<Position>(positionRepository.findByEnabledTrueOrderByNameAsc());
HashSet<Role> roles = new HashSet<Role>(roleRepository.findAll());
User employee = new User();
model.addAttribute("employee", employee);
model.addAttribute("allPositions", positions);
model.addAttribute("allRoles", roles);
}
}
This code is throwing TransactionRequiredException, I do not know why? It looks like #Transactional annotation did not work, so I moved annotation to processNewEmployee()
Second example:
#Controller
public class EmployeeController extends AbstractCrudController {
// rest of code (...)
#Transactional
#RequestMapping(value = urlFragment + "/create", method = RequestMethod.POST)
public String processNewEmployee(Model model, #ModelAttribute("employee") User employee, BindingResult result, HttpServletRequest request) {
prepareUserForm(model);
if (!result.hasErrors()) {
try {
entityManager.merge(employee);
model.addAttribute("success", true);
} catch (Exception e) {
model.addAttribute("error", true);
}
}
return "crud/employee/create";
}
private void prepareUserForm(Model model) { /*(.....)*/ }
}
And this code is throwing PersistenceException (because of ConstraintViolationException) and of course I got "Transaction marked as rollbackOnly" exeption.
When I try to save email which not exists this code works fine, so I thing that #Transactional annotation is configured well.
If this is important I am putting my TransationManagersConfig:
#Configuration
#EnableTransactionManagement
public class TransactionManagersConfig implements TransactionManagementConfigurer {
#Autowired
private EntityManagerFactory emf;
#Autowired
private DataSource dataSource;
#Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager tm =
new JpaTransactionManager();
tm.setEntityManagerFactory(emf);
tm.setDataSource(dataSource);
return tm;
}
public PlatformTransactionManager annotationDrivenTransactionManager() {
return transactionManager();
}
}
Could you explain my what I am doing wrong and suggest possible solution of this problem?
Solution:
Thanks to R4J I have created UserService and in my EmployeeController I am using it instead of entityManager.merge() now it works fine
#Service
public class UserService {
#PersistenceContext
private EntityManager entityManager;
#Transactional
public void merge(User user) {
entityManager.merge(user);
}
}
And EmployeeController:
#Controller
public class EmployeeController extends AbstractCrudController {
#Autowired
private UserService userService;
#RequestMapping(value = urlFragment + "/create", method = RequestMethod.POST)
public String processNewEmployee(Model model, #ModelAttribute("employee") User employee, BindingResult result, HttpServletRequest request) {
// (.....)
userService.merge(employee);
// (.....)
}
}
Your transactions don't work because you call directly 'this.saveEmployee(...)' from your 'public String processNewEmployee' method.
How come?
When you add #Transactional, Spring creates a Proxy for your Component and proxies all public methods. So when Spring itself calls your method as a HTTP Rest Request it is considered an external call that goes properly through a Proxy and new Transaction is started as required and code works.
But when you have a Proxied Component and you call 'this.saveEmployee' (that has #Transactional annotation) inside your class code you are actually bypassing the Proxy Spring has created and new Transaction is not started.
Solution:
Extract entire database logic to some sort of a Service or DAO and just Autowire it to your Rest Controller. Then everything should work like a charm.
You should avoid direct database access from Controllers anyway as it is not a very good practice. Controller should be as thin as possible and contain no business logic because it is just a 'way to access' your system. If your entire logic is in the 'domain' then you can add other ways to run business functionalities (like new user creation) in a matter of just few lines of code.