javaspringspring-bootspring-securitymockmvc

Spring Security + Mock MVC Testing - Assertion error, redirect URL "http://localhost/login" instead of "/login"


I have such a test

@Test
@WithAnonymousUser
void givenNoAuthentication_whenEnteringMainPage_thenRedirectToLoginPage() throws Exception {
    mockMvc.perform(get("/")).andExpect(redirectedUrl("/login"));
}

And I got assertion error like this:

java.lang.AssertionError: Redirected URL expected:</login> but was:<http://localhost/login>
Expected :/login
Actual   :http://localhost/login

What is more, I have another test similar to this one which tests if logged in user gets redirected to main page ("/") after accessing /login and this test is working fine

@Test
@WithMockUser
void givenAuthentication_whenEnteringLoginPage_thenRedirectToMainPage() throws Exception {
    mockMvc.perform(get("/login")).andExpect(redirectedUrl("/"));
}

Here is how my spring security config looks like

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
            .authorizeHttpRequests(authorize - > authorize
                .requestMatchers("/css/**").permitAll()
                .requestMatchers("/js/**").permitAll()
                .requestMatchers("/login").permitAll()
                .requestMatchers("/register").permitAll()
                .requestMatchers("/user/register").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form - > form
                .loginPage("/login")
                .defaultSuccessUrl("/", true)

            )
            .logout(logout - > logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login")
            )
            .build();
    }

    @Bean
    public static PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

How can I fix my first test, and why it doesnt't work when second test which is almost the same is working fine?

EDIT: I managed to fix this by using .endsWith("/login") like so

MvcResult result = mockMvc.perform(get("/")).andReturn();
assertThat(result.getResponse().getRedirectedUrl()).endsWith("/login");

But I still don't understand why in this test redirect url contains hostname, and it other it doesn't


Solution

  • While not a definitive answer, maybe my research will help someone as this behavior is a bit annoying when writing tests for /login, because that endpoint behaves differently.

    There are some related answers:

    I believe the behavior is intentional for two reasons

    1. The behavior only occurs on /login and no other endpoints. org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint.buildRedirectUrlToLoginPage is responsible for this behavior, there is explicit logic that makes the URL absolute (spring.security.web:6.2.1):
    if (UrlUtils.isAbsoluteUrl(loginForm)) {
        return loginForm;
    }
    // ...
    // otherwise construct absolute URL
    
    1. FormLoginConfigurerTests has asserts for redirectedUrl for login set explicitly to test absolute values everywhere. On all other places the tests uses relative paths.

    Speculations only:

    Maybe it's a bug or was written in that way due to historical reasons - e.g. older RFC 2616 stated to use absolute uris:

    Location = "Location" ":" absoluteURI

    but the newer RFC 7231 allows relative paths (details in this answer).

    Or maybe there is some security concern in mind - forcing to use absolute URI on login so that app have "more" under control whether http or https is used.

    Either way, I haven't found a way to configure it to behave differently. So right now I'm forced to test /login endpoints differently than other endpoints.