28. Reactive Test Support

28.1 Testing Reactive Method Security

For example, we can test our example from Chapter 27, EnableReactiveMethodSecurity using the same setup and annotations we did in Section 18.1, “Testing Method Security”. Here is a minimal sample of what we can do:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = HelloWebfluxMethodApplication.class)
public class HelloWorldMessageServiceTests {
    @Autowired
    HelloWorldMessageService messages;

    @Test
    public void messagesWhenNotAuthenticatedThenDenied() {
        StepVerifier.create(this.messages.findMessage())
            .expectError(AccessDeniedException.class)
            .verify();
    }

    @Test
    @WithMockUser
    public void messagesWhenUserThenDenied() {
        StepVerifier.create(this.messages.findMessage())
            .expectError(AccessDeniedException.class)
            .verify();
    }

    @Test
    @WithMockUser(roles = "ADMIN")
    public void messagesWhenAdminThenOk() {
        StepVerifier.create(this.messages.findMessage())
            .expectNext("Hello World!")
            .verifyComplete();
    }
}

28.2 WebTestClientSupport

Spring Security provides integration with WebTestClient. The basic setup looks like this:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = HelloWebfluxMethodApplication.class)
public class HelloWebfluxMethodApplicationTests {
    @Autowired
    ApplicationContext context;

    WebTestClient rest;

    @Before
    public void setup() {
        this.rest = WebTestClient
            .bindToApplicationContext(this.context)
            // add Spring Security test Support
            .apply(springSecurity())
            .configureClient()
            .filter(basicAuthentication())
            .build();
    }
    // ...
}

28.2.1 Authentication

After applying the Spring Security support to WebTestClient we can use either annotations or mutateWith support. For example:

@Test
public void messageWhenNotAuthenticated() throws Exception {
    this.rest
        .get()
        .uri("/message")
        .exchange()
        .expectStatus().isUnauthorized();
}

// --- WithMockUser ---

@Test
@WithMockUser
public void messageWhenWithMockUserThenForbidden() throws Exception {
    this.rest
        .get()
        .uri("/message")
        .exchange()
        .expectStatus().isEqualTo(HttpStatus.FORBIDDEN);
}

@Test
@WithMockUser(roles = "ADMIN")
public void messageWhenWithMockAdminThenOk() throws Exception {
    this.rest
        .get()
        .uri("/message")
        .exchange()
        .expectStatus().isOk()
        .expectBody(String.class).isEqualTo("Hello World!");
}

// --- mutateWith mockUser ---

@Test
public void messageWhenMutateWithMockUserThenForbidden() throws Exception {
    this.rest
        .mutateWith(mockUser())
        .get()
        .uri("/message")
        .exchange()
        .expectStatus().isEqualTo(HttpStatus.FORBIDDEN);
}

@Test
public void messageWhenMutateWithMockAdminThenOk() throws Exception {
    this.rest
        .mutateWith(mockUser().roles("ADMIN"))
        .get()
        .uri("/message")
        .exchange()
        .expectStatus().isOk()
        .expectBody(String.class).isEqualTo("Hello World!");
}

28.2.2 CSRF Support

Spring Security also provides support for CSRF testing with WebTestClient. For example:

this.rest
    // provide a valid CSRF token
    .mutateWith(csrf())
    .post()
    .uri("/login")
    ...

28.2.3 Testing Bearer Authentication

In order to make an authorized request on a resource server, you need a bearer token. If your resource server is configured for JWTs, then this would mean that the bearer token needs to be signed and then encoded according to the JWT specification. All of this can be quite daunting, especially when this isn’t the focus of your test.

Fortunately, there are a number of simple ways that you can overcome this difficulty and allow your tests to focus on authorization and not on representing bearer tokens. We’ll look at two of them now:

mockJwt() WebTestClientConfigurer

The first way is via a WebTestClientConfigurer. The simplest of these would look something like this:

client
    .mutateWith(mockJwt()).get().uri("/endpoint").exchange();

What this will do is create a mock Jwt, passing it correctly through any authentication APIs so that it’s available for your authorization mechanisms to verify.

By default, the JWT that it creates has the following characteristics:

{
  "headers" : { "alg" : "none" },
  "claims" : {
    "sub" : "user",
    "scope" : "read"
  }
}

And the resulting Jwt, were it tested, would pass in the following way:

assertThat(jwt.getTokenValue()).isEqualTo("token");
assertThat(jwt.getHeaders().get("alg")).isEqualTo("none");
assertThat(jwt.getSubject()).isEqualTo("sub");
GrantedAuthority authority = jwt.getAuthorities().iterator().next();
assertThat(authority.getAuthority()).isEqualTo("read");

These values can, of course be configured.

Any headers or claims can be configured with their corresponding methods:

client
    .mutateWith(mockJwt().jwt(jwt -> jwt.header("kid", "one")
        .claim("iss", "https://idp.example.org")))
    .get().uri("/endpoint").exchange();
client
    .mutateWith(mockJwt().jwt(jwt -> jwt.claims(claims -> claims.remove("scope"))))
    .get().uri("/endpoint").exchange();

The scope and scp claims are processed the same way here as they are in a normal bearer token request. However, this can be overridden simply by providing the list of GrantedAuthority instances that you need for your test:

client
    .mutateWith(jwt().authorities(new SimpleGrantedAuthority("SCOPE_messages")))
    .get().uri("/endpoint").exchange();

Or, if you have a custom Jwt to Collection<GrantedAuthority> converter, you can also use that to derive the authorities:

client
    .mutateWith(jwt().authorities(new MyConverter()))
    .get().uri("/endpoint").exchange();

You can also specify a complete Jwt, for which Jwt.Builder comes quite handy:

Jwt jwt = Jwt.withTokenValue("token")
    .header("alg", "none")
    .claim("sub", "user")
    .claim("scope", "read");

client
    .mutateWith(mockJwt().jwt(jwt))
    .get().uri("/endpoint").exchange();

authentication() WebTestClientConfigurer

The second way is by using the authentication() Mutator. Essentially, you can instantiate your own JwtAuthenticationToken and provide it in your test, like so:

Jwt jwt = Jwt.withTokenValue("token")
    .header("alg", "none")
    .claim("sub", "user")
    .build();
Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("SCOPE_read");
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities);

client
    .mutateWith(authentication(token))
    .get().uri("/endpoint").exchange();

Note that as an alternative to these, you can also mock the ReactiveJwtDecoder bean itself with a @MockBean annotation.