I'm writing some integration tests for my Spring MVC Controller.
The controllers are secured by Spring Security.
This is the test class I currently have:
#SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.MOCK,
classes = GuiBackendApplication.class
)
#AutoConfigureMockMvc
public class ConfigEditorControllerIntegrationTest {
#Autowired
private MockMvc mockedMvc;
#Test
#WithMockUser(username = "user", password = "password", roles = {"admin"})
public void adminsCanAccessRuntimeConfig() throws Exception {
this.mockedMvc.perform(get("/my/custom/api"))
.andExpect(status().isOk());
}
}
This test class ensures that admins can access my endpoint. It works fine.
BUT what if I want to test if ONLY users with the admin role can access my endpoint?
I could write a test that uses #WithMockUsers with all the roles I currently have except the admin role. But that would me awful to maintain. I want my test to ensure that only users with the admin role can access my endpoint, regardless of any new roles.
I checked the Spring Reference Docs and didn't find anything about that. Is there a way to achieve that?
Something like this
#Test
#WithMockUser(username = "user", password = "password", roles = {"IS NOT admin"})
public void nonAdminsCannotAccessRuntimeConfig() throws Exception {
this.mockedMvc.perform(get("/my/custom/api"))
.andExpect(status().isUnauthorized());
}
Spring Security does not know what roles does your system define. So you have to tell it and test it one by one if you want to have 100% test coverage for all the available roles.
You can do it easily and in a maintenance way by using JUnit 5 's #ParameterizedTest and configuring MockMvc with the UserRequestPostProcessor with different roles.
Something like :
public class ConfigEditorControllerIntegrationTest {
#ParameterizedTest
#MethodSource
public void nonAdminsCannotAccessRuntimeConfig(String role) throws Exception {
mockedMvc.perform(get("/my/custom/api")
.with(user("someUser").roles(role)))
.andExpect(status().isUnauthorized());
}
static List<String> nonAdminsCannotAccessRuntimeConfig() {
return Roles.exclude("admin");
}
}
And create a class to maintain all the available roles :
public class Roles {
public static List<String> all() {
return List.of("admin", "hr", "developer" , "accountant" , .... , "devops");
}
public static List<String> exclude(String excludeRole) {
List<String> result = new ArrayList<>(all());
result.remove(excludeRole);
return result;
}
}
Related
In many of my integration tests, I have to create Entities with a custom security context. Then within tests I use another security context to check for example the access rights. So I created a new annotation:
#Retention(RetentionPolicy.RUNTIME)
#WithSecurityContext(factory = CustomSecurityContextFactory.class)
public #interface WithCustomUser {
String username();
String[] roles() default {"USER"};
}
This is the CustomSecurityContextFactory class:
#Component
#RequiredArgsConstructor
public class CustomSecurityContextFactory implements WithSecurityContextFactory<WithCustomUser> {
// dependencies
#Override
public SecurityContext createSecurityContext(WithCustomUser withCustomUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
OAuth2AccessToken token = buildToken();
Employee employee = employeeService.findByUsername(withCustomUser.username());
Map<String, Object> attributes = new HashMap<>();
attributes.put("username", withCustomUser.username());
List<String> roles = List.of(withCustomUser.roles());
List<GrantedAuthority> authorities = buildAuthorities(roles, attributes);
employee.setAuthorities(authorities);
employee.setUsername(withCustomUser.username());
employee.setPassword(token.getTokenValue());
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(employee, token.getTokenValue(), authorities);
context.setAuthentication(authentication);
return context;
}
}
I can now easily use the annotation above test methods like this:
#Test
#WithCustomUser(username = "myUsername")
void simpleTest() {
Order order = orderService.createOrder();
assertThat(order.getEmployee().getUsername()).isEqualTo("myUsername");
}
This works great. but I would like to create the order in beforeAll method and use it in the tests. something like this:
#BeforeAll
#WithCustomUser(username = "myUsername")
void beforeAllNonStatic() {
Order order = orderService.createOrder();
}
But this obviously doesn't work. when it tries to create the order, employee is null. I currently use static mocking in beforeAll method and that works. but is there a way to make the annotation work? Am I using the annotation at the wrong place?
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.
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() {
...
Or more specifically, is it possible?
We currently have our users in memory using XML configuration. We know of InMemoryUserDetailsManager, but unfortunately it's not possible to get all users and users map inside InMemoryUserDetailsManager is private.
Yes you can actually access that using a bit a jugglery with Java reflection where you can access the properties which are not exposed via public API.
I have used below the RelectionUtils from Spring which should be available if you are using Spring ( which you are since it's Spring security).
The key is to get hold of AuthenticationManager via Autowiring and then drill down to the required Map containing the user info.
Just to demonstrate I have tested this on Spring Boot App but there should not be any issue if your are not using it.
#SpringBootApplication
public class SpringBootSecurityInMemoryApplication implements CommandLineRunner {
#Autowired AuthenticationManager authenticationManager;
................................
...............................
public static void main(String[] args) {
SpringApplication.run(SpringBootSecurityInMemoryApplication.class, args);
}
#Override
public void run(String... args) throws Exception {
introspectBean(authenticationManager);
}
public void printUsersMap(Object bean){
Field field = ReflectionUtils.findField(org.springframework.security.authentication.ProviderManager.class, "providers");
ReflectionUtils.makeAccessible(field);
List listOfProviders = (List)ReflectionUtils.getField(field, bean);
DaoAuthenticationProvider dao = (DaoAuthenticationProvider)listOfProviders.get(0);
Field fieldUserDetailService = ReflectionUtils.findField(DaoAuthenticationProvider.class, "userDetailsService");
ReflectionUtils.makeAccessible(fieldUserDetailService);
InMemoryUserDetailsManager userDet = (InMemoryUserDetailsManager)(ReflectionUtils.getField(fieldUserDetailService, dao));
Field usersMapField = ReflectionUtils.findField(InMemoryUserDetailsManager.class, "users");
ReflectionUtils.makeAccessible(usersMapField);
Map map = (Map)ReflectionUtils.getField(usersMapField, userDet);
System.out.println(map);
}
I have 2 users configured - shailendra and admin. You can see the output of program below. You can get the required info from this map.
{shailendra=org.springframework.security.provisioning.MutableUser#245a060f, admin=org.springframework.security.provisioning.MutableUser#6edaa77a}
I am using cucumber tests to test my spring boot app with spring security enabled .Things work fine except when I run my test suite with cucumber tests some tests using spring security eg.
#WithMockUser(username = "BROWSER", roles =
{"BROWSER","ADMIN"})
fail .These tests do work if I do run them in seclusion as simple junit tests but fail when run with cucumber test steps.
The issue looks like the spring security test mock behaviour isnt getting applied when I run the same with cucumber tests.
My cucumber test run class is as below
#RunWith(Cucumber.class)
#CucumberOptions(features = "src/test/resources", monochrome = true, format =
{"pretty", "html:src/main/resources/static/cucumber"})
public class CucumberTests
{
}
Also I noticed the same works when run via Maven with <reuseForks>false</reuseForks> .Also maven triggered test case run also fails if this option is not checked .
UPDATE
AbstractIntegrationTest class all tests extend
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = Services.class,loader = SpringApplicationContextLoader.class)
//#IntegrationTest
#WebIntegrationTest(randomPort = true)
public abstract class AbstractIntegrationTest {
Another use case which does not work is using theses annotation is cucumber feature conditions like below
#When("^I apply a GET on (.*)$")
#WithMockUser(username = "BROWSER", roles = { "BROWSER", "ADMIN" })
public void i_search_with_rsql(String query) throws Throwable {
result = mvc.perform(get(uri, query));
}
any help or workaround on this.
WithMockUser does not work with Cucumber. Use cucumber hooks instead.
WithMockUser relies on TestExecutionListener#beforeTestMethod from Spring's test context support, but they are not invoked when running with Cucumber runner. This is because Cucumber runs scenarios composed of steps rather than the standard JUnit test methods.
Option 1 - Security context hooks. You can setup security context with hooks, for example:
#ActiveProfiles("test")
#SpringBootTest(classes = MyServer.class)
#AutoConfigureMockMvc
#AutoConfigureRestDocs
#AutoConfigureCache
public class MyServerContextHooks {
#Before
public void onBeforeScenario(final Scenario scenario) {
// This method does nothing - context setup is done with annotations
}
}
Example annotation on scenarios:
#WithAdminUser
Scenario: Run action as admin
...
Example hook to use annotation on scenarios:
public class TestUserHooks {
#Before("#WithAdminUser")
public void setupAdminUser() {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
"admin",
"N/A",
createAuthorityList("admin")));
}
}
Option 2 - Authentication steps. Another way is to use special steps for providing user into mockMvc:
Scenario: Run action as admin
Given I am logged in as admin
...
Stepdef example:
public class SecurityTestSteps {
#Autowired
private MockMvcGlue mockMvc;
#Autowired
private OAuth2Mocks oauth2Mocks;
#Autowired
private TestUsers users;
/**
* Provides a one of predefined role-based authentications for the current request.
*/
#Given("^I am logged in as (admin|editor|user)$")
public void given_UserIsAuthenticatedWithRole(final String role) {
switch (role) {
case "admin":
mockMvc.request().with(authentication(oauth2Mocks.auth(users.admin())));
break;
case "editor":
mockMvc.request().with(authentication(oauth2Mocks.auth(users.edtior())));
break;
default:
throw new CucumberException("Unsupported role <" + role + ">");
}
}
}
Base upon your comments you need to ensure to apply Spring Security. You can find an example of this in the Setting Up MockMvc and Spring Security section of the reference documentation:
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
// ...
#Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity()) // ADD THIS!
.build();
}
In Response to Rob Winch's answer, mine worked using his method minus the line
".apply(springSecurity())"