I am trying to implement authorization for app endpoints. I am working on using custom methods inside @PreAuthorize
annotation.
@RestController
@RequestMapping("test")
@RequiredArgsConstructor
public class TestController {
private final TestService testService;
@GetMapping("/{userId}")
@ResponseStatus(HttpStatus.OK)
@PreAuthorize("@SecurityService.hasAccessToUser(#userId)")
List<TestDTO> getUserData(@PathVariable String userId) {
return testService.fetchUserData(userId);
}
}
sidenote - presented below hasAccessToUser
method could be replaced by simply using @PreAuthorize("authorization.principal == #userId")
but it's just a stripped-down version of SecurityService
@Service
@RequiredArgsConstructor
public class SecurityService {
private Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
private String getUserId() {
return (String) getAuthentication().getPrincipal();
}
public boolean hasAccessToUser(String userId) {
return getUserId().equals(userId);
}
}
This authorization implementation works well on launched application, BUT it breaks while testing:
@WebMvcTest(TestController.class)
public class TestControllerTest {
private static final String BASE_URL = "/test";
@Autowired
private MockMvc mockMvc;
@MockBean
private TestService testService;
@MockBean
private SecurityService securityService;
@MockBean
private TestRepository testRepository;
@SneakyThrows
@Test
void shouldGetUserDataGivenUserId() {
mockMvc.perform(asUser(get(BASE_URL "/" TEST_USER_ID)))
.andExpect(status().is(Response.SC_OK));
verify(testService).fetchUserData(TEST_USER_ID);
}
}
When I look into a body of a response to the failed request it has lengthy log ending with:
Internal Server Error: Failed to evaluate expression '@SecurityService.hasAccessToUser(#userId)'"}]
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:59)
at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:122)
at org.springframework.test.web.servlet.result.ContentResultMatchers.lambda$string$4(ContentResultMatchers.java:148)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:212)
at com.test.controller.TestControllerTest.shouldGetUserDataGivenUserId(TestControllerTest.java:87)
at java.base/java.util.ArrayList.forEach(Unknown Source)
So, Spring does not recognize @SecurityService
while running tests. Is there any way of making @SecurityService
work inside @PreAuthorize
during tests?
CodePudding user response:
The expression @SecurityService.hasAccessToUser(#userId)
will look for a bean with the name SecurityService
. But you are now using @MockBean
to define this bean which by default its bean name will be auto generated. There are no beans which the name is SecurityService
and so it throws this error.
You can explicitly configure its bean name by :
@MockBean(name="SecurityService")
private SecurityService securityService;
which should solve your problem.
But better yet , don't mock SecurityService
because it is a part of your application security setting and is not a kind of collaborator or dependency of the controller under test. Use the real instance can make the test case more close to the actual production codes.
@WebMvcTest(TestController.class)
@Import({TestController.Config.class})
public class TestControllerTest {
@Configuration
public static class Config {
@Bean(name="SecurityService")
public SecurityService securityService() {
return new SecurityService();
}
}
}