I would say "certainly not use test container during unit tests". This is way slower:
- with a test container, in addition to lifting the container, you request for valid tokens over the network, decode it and then build the
Authentication
instance to put in the security-context
- with MockMvc request postprocessors (the
.with(jwt())
you are using), WebTestClient mutators and test annotations, build the Authentication
directly (no token is created nor decoded, nor validated, we skip directly to the result: building the Authentication
instance).
Also, with test annotations, you can setup security when unit-testing other components than @Controller
. Consider the following for instance:
@Service
public class SecuredService {
@PreAuthorize("hasAuthority('NICE')")
String nice() {
return "Dear %s, glad to see you!".formatted(SecurityContextHolder.getContext().getAuthentication().getName());
}
@PreAuthorize("isAuthenticated()")
String hello() {
return "Hello %s.".formatted(SecurityContextHolder.getContext().getAuthentication().getName());
}
}
With RestAssured
and a test container, the security-context is built as part of the request filtering. But it makes no sense to send an HTTP request to unit-test a @Service
.
Instead of the MockMvc post processor I contributed to spring-security-test
, you might consider using the test annotations I publish in spring-addons-oauth2-test
.
With "my" annotations, you have different choices to test the service above (note the compatibility with JUnit 5 @ParameterizedTest
):
- if defining just authorities (and maybe name and
Authentication
type to build), @WithMockAuthentication
is probably enough:
@SpringBootTest(classes = { SecurityConfig.class, MessageService.class })
class MessageServiceTests {
@Autowired
private SecuredService securedService;
@Test
@WithMockAuthentication("BAD_BOY")
void givenUserIsNotGrantedWithNice_whenCallNice_thenThrows() {
assertThrows(Exception.class, () -> securedService.nice());
}
@Test
@WithMockAuthentication(name = "brice", authorities = "NICE")
void givenUserIsNice_whenCallNice_thenReturnsGreeting() {
assertThat(securedService.nice()).isEqualTo("Dear brice, glad to see you!");
}
@ParameterizedTest
@AuthenticationSource(
@WithMockAuthentication(name = "brice", authorities = "NICE"),
@WithMockAuthentication(name = "ch4mp", authorities = { "VERY_NICE", "AUTHOR" }))
void givenUserIsAuthenticated_whenCallHello_thenReturnsGreeting(@ParameterizedAuthentication Authentication auth) {
assertThat(securedService.hello()).isEqualTo("Hello %s.".formatted(auth.getName()));
}
}
- if you want a complete control on all claims, then
@WithJwt
(or @WithOpaqueToken
if you're using introspection) might be better suited:
@AddonsWebmvcComponentTest // omit if you're not using the starter, this loads a minimal subset of spring-addons security conf
@SpringBootTest(classes = { SecurityConfig.class, MessageService.class })
class MessageServiceTests {
@Autowired
private SecuredService securedService;
@Autowired
WithJwt.AuthenticationFactory authFactory;
@Test
@WithJwt("igor.json")
void givenUserIsIgor_whenCallNice_thenThrows() {
assertThrows(Exception.class, () -> securedService.nice());
}
@Test
@WithJwt("brice.json")
void givenUserIsBrice_whenCallNice_thenReturnsGreeting() {
assertThat(securedService.nice()).isEqualTo("Dear brice, glad to see you!");
}
@ParameterizedTest
@MethodSource("identities")
void givenUserIsAuthenticated_whenCallHello_thenReturnsGreeting(@ParameterizedAuthentication Authentication auth) {
assertThat(securedService.hello()).isEqualTo("Hello %s.".formatted(auth.getName()));
}
Stream<AbstractAuthenticationToken> identities() {
return authFactory.authenticationsFrom("brice.json", "igor.json");
}
}
@WithJwt
will search for an authentication converter in your security conf (a Converter<Jwt, ? extends AbstractAuthenticationToken>
as you most probably have defined to turn Keycloak roles into Spring Security authorities) and use it to build an Authentication instance out of a JSON payload in the test class-ath (inspired by a JWT payload or introspection response). So basically, the Authentication instance should be the exact same as with a real authorization server delivering a token with the same claims (name, authorities, Authentication impl, etc.).