I have a spring boot application which requires login for some actions. I am trying to test them using MockMvc, but it doesn't seem to work. I keep getting a HTTP response with status 403 (forbidden). Probably there is something wrong with the authentication part.
I have tried following the documentation, but I wasn't able to get it working.
This is my current testing code:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = {Application.class})
#WebIntegrationTest("server.port = 8093")
public class PasswordChangeTests {
#Autowired
private EmbeddedWebApplicationContext webApplicationContext;
#Autowired
private UserRepository userRepository;
private MockMvc mockMvc;
#Before
public void setUp() throws Exception {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(webApplicationContext)
.apply(springSecurity())
.build();
}
#Test
public void changePasswordWorks() throws Exception {
// Send password change request
PasswordChangeRepresentation passwordChange = new PasswordChangeRepresentation(DefaultUsers.Admin.getPassword(), "12345678");
mockMvc.perform(MockMvcRequestBuilders.request(HttpMethod.POST, "/password/change")
.content(new ObjectMapper().writeValueAsString(passwordChange))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
// Check that the password has been changed
User user = this.userRepository.findByUsername(DefaultUsers.Admin.getEmail());
Assert.assertEquals(user.getPassword(), "12345678");
}
}
Sorry if I am missing something obvious. This is my first experience with spring boot.
You need to specify which user you want to run the test as. You have a few options (each option is a link to the detailed documentation):
#WithMockUser
This option will create a fake user (i.e. the user does not need to exist in a data store). The problem with this approach is if your application relies on a custom User implementation you may get class cast Exceptions. If you do not return a custom type from a custom UserDetailsService, then this solution should work fine.
#Test
#WithMockUser(username="admin",roles={"USER","ADMIN"})
public void changePasswordWorks() throws Exception {
#WithUserDetails
If you implemented a custom UserDetailsService that returns a custom implementation of UserDetails, this solution may work for you.
For it to work you need to expose a UserDetailsService as a Bean and the user must exist. For example:
#Test
#WithUserDetails("admin")
public void changePasswordWorks() throws Exception {
#WithSecurityContext
This is the best of both worlds, but requires a little additional setup. If you have a custom UserDetailsService returning a custom implementation of UserDetails and do NOT want the user to necessarily have to exist you can use this method. I'll let you read the documentation on this setup as it is a bit more lengthy and well documented.
Using a RequestPostProcessor
If annotations aren't your thing you can use a RequestPostProcessor. For example:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
...
#Test
public void changePasswordWorks() throws Exception {
// Send password change request
PasswordChangeRepresentation passwordChange = new PasswordChangeRepresentation(DefaultUsers.Admin.getPassword(), "12345678");
mockMvc.perform(MockMvcRequestBuilders.request(HttpMethod.POST, "/password/change")
// ADD this line
.with(user("admin").roles("USER","ADMIN"))
.content(new ObjectMapper().writeValueAsString(passwordChange))
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
// Check that the password has been changed
User user = this.userRepository.findByUsername(DefaultUsers.Admin.getEmail());
Assert.assertEquals(user.getPassword(), "12345678");
}
Related
CheckoutController
#Controller
#Profile("!test")
public class CheckoutController {
private MonetaryAmount total = Money.of(0, EURO);
private final UniqueInventory<UniqueInventoryItem> inventory;
private final Katalog catalogue;
private List<UniqueInventoryItem> history = new ArrayList();
#Autowired
public CheckoutController(UniqueInventory<UniqueInventoryItem> _inventory, Katalog _catalogue){
inventory = _inventory;
catalogue = _catalogue;
}
//! Get Mappings
//#GetMapping("/checkout")
#RequestMapping("/checkout")
#PreAuthorize("hasRole('WORKER')")
public String checkout(Model model){
model.addAttribute("total", this.total);
model.addAttribute("history", this.history);
model.addAttribute("controller", this);
return "checkout"; // thymleafe located in proper path, visible when logged in
}
}
TestingController
#SpringBootTest
#AutoConfigureMockMvc
#ActiveProfiles("test")
public class CheckoutControllerTests {
#Autowired
MockMvc mockMvc;
#BeforeEach
public void pre() {
// code
}
#Test
public void checkout() throws Exception {
// TODO Error msg: status expected:<200> but was:<404>
mockMvc.perform(get("/checkout").with(user("paul").roles("WORKER")))
.andExpect(status().isOk());
}
}
Where it says "/checkout" above, I could put all other available routes which are accepted, but not this one and I do not know why. Again, it's visible to my once logged in running the project.
I tried using RequestMapping instead of GetMapping but that didnt work either. I googled with no success, in most cases people did not actually point to the right html file but thats not the case here either. I am lost at this point, asked my friends and colleagues with no success.
If you have any clue what it could be, improper mvc setup, yadada, please let me know!
You have conflicting profiles. Your controller is annotated with #Profile("!test") while your test is executing with #ActiveProfiles("test").
tl;dr:
Seems like the Mock of the repository I created with custom behavior regarding the save method when injected loses the custom behavior.
Problem Description
I've been trying to test a Service in Spring. The method of interest in particular takes some parameters and creates a User that is saved into a UserRepository through the repository method save.
The test I am interest in making is comparing these parameters to the properties of the User passed to the save method of the repository and in this way check if it is properly adding a new user.
For that I decided to Mock the repository and save the param passed by the service method in question to the repository save method.
I based myself on this question to save the User.
private static User savedUser;
public UserRepository createMockRepo() {
UserRepository mockRepo = mock(UserRepository.class);
try {
doAnswer(new Answer<Void>() {
#Override
public Void answer(InvocationOnMock invocation) throws Throwable {
savedUser= (User) invocation.getArguments(0);
return null;
}
}).when(mockRepo).save(any(User.class));
} catch( Exception e) {}
return mockRepo;
}
private UserRepository repo = createMockRepo();
Two notes:
I gave the name repo in case the name had to match the one in the service.
There is no #Mock annotation since it starts failing the test, I presume that is because it will create a mock in the usual way (without the custom method I created earlier).
I then created a test function to check if it had the desired behavior and all was good.
#Test
void testRepo() {
User u = new User();
repo.save(u);
assertSame(u, savedUser);
}
Then I tried doing what I saw recommended across multiple questions, that is, to inject the mock into the service as explained here.
#InjectMocks
private UserService service = new UserService();
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
This is where the problems arise, the test I created for it throws a null exception when I try to access savedUser properties (here I simplified the users properties since that doesn't seem to be the cause).
#Test
void testUser() {
String name = "Steve";
String food = "Apple";
service.newUser(name, food);
assertEquals(savedUser.getName(), name);
assertEquals(savedUser.getFood(), food);
}
Upon debugging:
the service seems to have received the mock: debugged properties of the service
the savedUser is indeed null: debugged savedUser propert .
I decided to log the function with System.out.println for demonstrative purposes.
A print of my logging of the tests, demonstrating that the user test doesn't call the answer method
What am I doing wrong here?
Thank you for the help in advance, this is my first stack exchange question any tips for improvement are highly appreciated.
Instead of instanciating your service in the test class like you did, use #Autowired and make sure your UserRepository has #MockBean in the test class
#InjectMocks
#Autowired
private UserService service
#MockBean
private UserRepository mockUserRepo
With this, you can remove your setup method
But make sure your UserRepository is also autowired insider your Service
You should not need Spring to test of this. If you are following Spring best practicies when it comes to autowiring dependencies you should be able just create the objects yourself and pass the UserRepository to the UserService
Best practices being,
Constructor injection for required beans
Setter injection for optional beans
Field injection never unless you cannot inject to a constructor or setter, which is very very rare.
Note that InjectMocks is not a dependency injection framework and I discourage its use. You can see in the javadoc that it can get fairly complex when it comes to constructor vs. setter vs. field.
Note that working examples of the code here can be found in this GitHub repo.
A simple way to clean up your code and enable it to be more easily tested would be to correct the UserService to allow you to pass whatever implementation of a UserRepository you want, this also allows you to gaurentee immuability,
public class UserService {
public UserService(final UserRepository userRepository) {
this.userRepository = userRepository;
}
public final UserRepository userRepository;
public User newUser(String name, String food) {
var user = new User();
user.setName(name);
user.setFood(food);
return userRepository.save(user);
}
}
and then your test would be made more simple,
class UserServiceTest {
private UserService userService;
private UserRepository userRepository;
private static User savedUser;
#BeforeEach
void setup() {
userRepository = createMockRepo();
userService = new UserService(userRepository);
}
#Test
void testSaveUser(){
String name = "Steve";
String food = "Apple";
userService.newUser(name, food);
assertEquals(savedUser.getName(), name);
assertEquals(savedUser.getFood(), food);
}
public UserRepository createMockRepo() {
UserRepository mockRepo = mock(UserRepository.class);
try {
doAnswer(
(Answer<Void>) invocation -> {
savedUser = (User) invocation.getArguments()[0];
return null;
})
.when(mockRepo)
.save(any(User.class));
} catch (Exception e) {
}
return mockRepo;
}
}
However, this doesn't add a lot of benefit in my opinion as you are interacting with the repository directly in the service unless you fully understand the complexity of a Spring Data Repository, you are after all also mocking networking I/O which is a dangerous thing to do
How do #Id annotations work?
What about Hibernate JPA interact with my Entitiy?
Do my column definitions on my Entitiy match what I would deploy against when
using something like Liquibase/Flyway to manage the database
migrations?
How do I test against any constraints the database might have?
How do I test custom transactional boundaries?
You're baking in a lot of assumptions, to that end you could use the #DataJpaTest documentation annotation that Spring Boot provides, or replicate the configuration. A this point I am assuming a Spring Boot application, but the same concept applies to Spring Framework applications you just need to setup the configurations etc. yourself.
#DataJpaTest
class BetterUserServiceTest {
private UserService userService;
#BeforeEach
void setup(#Autowired UserRepository userRepository) {
userService = new UserService(userRepository);
}
#Test
void saveUser() {
String name = "Steve";
String food = "Apple";
User savedUser = userService.newUser(name, food);
assertEquals(savedUser.getName(), name);
assertEquals(savedUser.getFood(), food);
}
}
In this example we've went a step further and removed any notion of mocking and are connecting to an in-memory database and verifying the user that is returned is not changed to what we saved.
Yet there are limitations with in-memory databases for testing, as we are normally deploying against something like MySQL, DB2, Postgres etc. where column definitions (for example) cannot accurately be recreated by an in-memory database for each "real" database.
We could take it a step further and use Testcontainers to spin up a docker image of a database that we would connecting to at runtime and connect to it within the test
#DataJpaTest
#Testcontainers(disabledWithoutDocker = true)
class BestUserServiceTest {
private UserService userService;
#BeforeEach
void setup(#Autowired UserRepository userRepository) {
userService = new UserService(userRepository);
}
#Container private static final MySQLContainer<?> MY_SQL_CONTAINER = new MySQLContainer<>();
#DynamicPropertySource
static void setMySqlProperties(DynamicPropertyRegistry properties) {
properties.add("spring.datasource.username", MY_SQL_CONTAINER::getUsername);
properties.add("spring.datasource.password", MY_SQL_CONTAINER::getPassword);
properties.add("spring.datasource.url", MY_SQL_CONTAINER::getJdbcUrl);
}
#Test
void saveUser() {
String name = "Steve";
String food = "Apple";
User savedUser = userService.newUser(name, food);
assertEquals(savedUser.getName(), name);
assertEquals(savedUser.getFood(), food);
}
}
Now we are accurately testing we can save, and get our user against a real MySQL database. If we took it a step further and introduced changelogs etc. those could also be captured in these tests.
I am using Mockito to test one of my get-mappings in the controller class. Here is my get method
#PostMapping(value = "insert/carbooking")
public ResponseEntity<Void> reservation(#Valid BookingRequest bookRequest) {
return validate(bookRequest, carService::booking);
}
At the top of my class is my Validator
#Autowired
private ReservationValidator reservationValidator;
#InitBinder("bookRequest")
protected void bookRequestBinder(WebDataBinder binder) {
binder.addValidators(reservationValidator);
}
Here is the Mockito test method. The result should have returned bad request since the sin is in wrong format.
#Test
public void reservationTest2() throws Exception {
mockMvc.perform(MockMvcRequestBuilders
.post("insert/carbooking")
.param("license", "data")
.param("SIN", "202007191517")
.accept(MediaType.MULTIPART_FORM_DATA))
.andExpect(status().isBadRequest());
}
But the test fail
java.lang.AssertionError: Status expected:<400> but was:<200>
Expected :400
Actual :200
Is there any way for the Mockito to receive the "reservationValidator" ?
I got everything fixed thanks to #chrylis-onstrike- . For the source code, I just need to remove #Before addholders set up method and replace it with the annotation #Autowired on Mockmvc mockmvc and #MockBean on service class which put the client class on the application context. It's such a shame I could not give tick mark to his answer since he didn't post the answer to my question rather commenting on it.
First of all, I have the following endpoint method present within a class called RecipeController:
#RequestMapping(value = {"/", "/recipes"})
public String listRecipes(Model model, Principal principal){
List<Recipe> recipes;
User user = (User)((UsernamePasswordAuthenticationToken)principal).getPrincipal();
User actualUser = userService.findByUsername(user.getUsername());
if(!model.containsAttribute("recipes")){
recipes = recipeService.findAll();
model.addAttribute("nullAndNonNullUserFavoriteRecipeList",
UtilityMethods.nullAndNonNullUserFavoriteRecipeList(recipes, actualUser.getFavoritedRecipes()));
model.addAttribute("recipes", recipes);
}
if(!model.containsAttribute("recipe")){
model.addAttribute("recipe", new Recipe());
}
model.addAttribute("categories", Category.values());
model.addAttribute("username", user.getUsername());
return "recipe/index";
}
As you can see above, the method takes as a second parameter a Principal object. When running the application, the parameter points to a non-null object as expected. It contains information about the user that is currently logged in within the application.
I have created a test class for the RecipeController called RecipeControllerTest. This class contains a single method called testListRecipes.
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration
#WebAppConfiguration
public class RecipeControllerTest{
#Mock
private RecipeService recipeService;
#Mock
private IngredientService ingredientService;
#Mock
private StepService stepService;
#Mock
private UserService userService;
#Mock
private UsernamePasswordAuthenticationToken principal;
private RecipeController recipeController;
private MockMvc mockMvc;
#Before
public void setUp(){
MockitoAnnotations.initMocks(this);
recipeController = new RecipeController(recipeService,
ingredientService, stepService, userService);
mockMvc = MockMvcBuilders.standaloneSetup(recipeController).build();
}
#Test
public void testListRecipes() throws Exception {
User user = new User();
List<Recipe> recipes = new ArrayList<>();
Recipe recipe = new Recipe();
recipes.add(recipe);
when(principal.getPrincipal()).thenReturn(user);
when(userService.findByUsername(anyString()))
.thenReturn(user);
when(recipeService.findAll()).thenReturn(recipes);
mockMvc.perform(get("/recipes"))
.andExpect(status().isOk())
.andExpect(view().name("recipe/index"))
.andExpect(model().attributeExists("recipes"))
.andExpect(model().attributeExists("recipe"))
.andExpect(model().attributeExists("categories"))
.andExpect(model().attributeExists("username"));
verify(userService, times(1)).findByUsername(anyString());
verify(recipeService, times(1)).findAll();
}
}
As you can see in this second snippet, I tried to mock the Principal object within the test class, using the UsernamePasswordAuthenticationToken implementation.
When I run the test, I get a NullPointerException, and the stacktrace points me to the following line from the first snippet of code:
User user = (User)((UsernamePasswordAuthenticationToken)principal).getPrincipal();
The principal object passed as a parameter to the listRecipes method from is still null, even though I tried to provide a mock object.
Any suggestions ?
Create a class that implements Principal:
class PrincipalImpl implements Principal {
#Override
public String getName() {
return "XXXXXXX";
}
}
Sample test:
#Test
public void login() throws Exception {
Principal principal = new PrincipalImpl();
mockMvc.perform(get("/login").principal(principal)).andExpect(.........;
}
Spring MVC is very flexible with controller arguments, which lets you put most of the responsibility of looking up information onto the framework and focus on writing the business code. In this particular case, while you can use Principal as a method parameter, it's usually much better to use your actual principal class:
public String listRecipes(Model model, #AuthenticationPrincipal User user)
To actually set the user for a test, you need to work with Spring Security, which means adding .apply(springSecurity()) to your setup. (Complications like this, by the way, are the main reason I dislike using standaloneSetup, as it requires you to remember to duplicate your exact production setup. I recommend writing actual unit tests and/or full-stack tests.) Then annotate your test with #WithUserDetails and specify the username of the test user.
Finally, as a side note this controller pattern can be simplified significantly with Querydsl, as Spring is able to inject a Predicate that combines all of the filter attributes you're looking up by hand, and then you can pass that predicate to a Spring Data repository.
Did you try using...?
#Test
#WithMockUser(username = "my_principal")
public void testListRecipes() {
...
I have been trying to write a test case for the following line of code but I keep getting java.lang.NullPointerException, I have tried to follow/replicate what others have suggested here Unit testing with Spring Security but I have had no luck. Can someone please help me better identify or give me a hint what I need to do. (I'm using mockito for this)
Code:
if (SecurityContextHolder.getContext().getAuthentication().getPrincipal().equals(user)) {
continue;
}
Test case:
#Test
public void testExpireAllSession() throws Exception {
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication().getPrincipal().equals(any(Object.class))).thenReturn(false);
SecurityContextHolder.setContext(securityContext);
controller.theMEthodUnderTest();
}
..
There are 2 problems with your test :
You must mock each "level" of method calls, you should mock :
SecurityContext.getAuthentication()
Authentication.getPrincipal()
Principal.equals()
But, you can't mock .equals(), see Mockito FAQ - limitations and Mockito - Issue 61.
You have to design your code/test differently. For example, pass a 'user' principal to your method arguments, and make Authentication.getPrincipal() return another one (they will be different, thus making the equals return false) :
Code
public void theMethod(Principal user) {
...
if (SecurityContextHolder.getContext().getAuthentication().getPrincipal().equals(user)) {
continue;
}
...
}
Test
#Test public void testController() {
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Authentication authentication = Mockito.mock(Authentication.class);
Principal principal1 = Mockito.mock(Principal.class);
Principal principal2 = Mockito.mock(Principal.class);
Mockito.when(authentication.getPrincipal()).thenReturn(principal1);
Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
SecurityContextHolder.setContext(securityContext);
new Controller().theMethod(principal2);
}
Few important things I did to make this work and hope this helps others as well.
Used the #InjectMocks :
#InjectMocks
private static YourMainController controller;
Mocked the dependencies which would get added to the main mock above:
#Mock
SecurityContext securityContextMocked;
#Mock
Authentication authenticationMocked;
#Mock
Principal principal1;
Modified the test to look like this and that made it work nicely.
#Test
public void testExpireAllSession() throws Exception {
List mySessions = new ArrayList<>();
Object principal="";
Date aDate = new Date();
SessionInformation sessionInformation = new SessionInformation(principal,”100000”,aDate);
mySessions.add(sessionInformation);
allUsers.add("Mike");
when(authenticationMocked.getPrincipal()).thenReturn(principal1);
when(securityContextMocked.getAuthentication()).thenReturn(authenticationMocked);
SecurityContextHolder.setContext(securityContextMocked);
when(sessionRegistryMocked.getAllSessions(allUsers,false)).thenReturn(sessions);
when(sessionRegistryMocked.getAllPrincipals()).thenReturn(allUsers);
controller.expireAllSession();
verify(sessionRegistryMocked).getAllPrincipals();
}