springspring-bootkotlinspring-securitywebtestclient

"could not resolve view" with `@PreAuthorize` and `WebTestClient`


I'm getting this rather weird bug in a test. My controller has the following method:

    @Operation(
        summary = "Add a new quote",
        operationId = "v1AddQuote",
        description = "",
        responses = [
            ApiResponse(responseCode = "201", description = "Created")
        ]
    )
    @RequestMapping(
            method = [RequestMethod.POST],
            value = ["/quote"],
            consumes = ["application/json"]
    )
    @PreAuthorize("hasRole('ADMIN')")
    suspend fun v1AddQuote(quoteDto: QuoteDto): ResponseEntity<Unit> {
        val entity = quoteRepository.insert(quoteMapper.dtoToEntity(quoteDto))
            .awaitSingleOrNull() ?: return ResponseEntity.internalServerError().build()

        return ResponseEntity.created(URI.create("/quote/${entity.id}")).build()
    }

And I'm testing it with WebTestClient with:

            webClient
                .mutateWith(mockUser().roles("ADMIN"))
                .post()
                .uri("/quote")
                .contentType(MediaType.APPLICATION_JSON)
                .body(Mono.just(dto), QuoteDto::class.java)
                .exchange()
                .expectStatus()
                .isCreated
                .expectHeader()
                .location("/quote/${entity.id}")

Surprisingly, this fails with:

java.lang.IllegalStateException: Could not resolve view with name 'quote'

Where does this even come from? Some details:

Looking at the stack trace it makes me think this is some sort of redirection Spring Security does?

    *__checkpoint ⇢ Handler jdk.proxy2.$Proxy126#v1AddQuote(QuoteDto, Continuation) [DispatcherHandler]
    *__checkpoint ⇢ org.springframework.security.web.server.authorization.AuthorizationWebFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ org.springframework.security.web.server.authorization.ExceptionTranslationWebFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ org.springframework.security.web.server.context.ReactorContextWebFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ org.springframework.security.web.server.header.HttpHeaderWriterWebFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ org.springframework.security.config.web.server.ServerHttpSecurity$ServerWebExchangeReactorContextWebFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ org.springframework.security.web.server.WebFilterChainProxy [DefaultWebFilterChain]
    *__checkpoint ⇢ org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers$MutatorFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers$SetupMutatorFilter [DefaultWebFilterChain]
    *__checkpoint ⇢ HTTP POST "/quote" [ExceptionHandlingWebHandler]

In case it's necessary, my Spring Security configuration is:

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class WebFluxSecurityConfig {

    @Bean
    fun userDetailsService(): ReactiveUserDetailsService {
        val userDetails = User.withDefaultPasswordEncoder()
            .username("admin")
            .password("admin")
            .roles("ADMIN")
            .build()
        return MapReactiveUserDetailsService(userDetails)
    }

    @Bean
    fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        return http.invoke {
            authorizeExchange {
                authorize(anyExchange, permitAll)
            }
            csrf { disable() }
            httpBasic { }
            formLogin { disable() }
            logout { disable() }
        }
    }
}

Solution

  • The problem was actually mostly unrelated to any of the info I gave on the post, and actually solved it almost by pure chance. The issue is that I was inheriting an OpenAPI-generated interface in my controller. I don't know why this gives such a weird error, but the solution is, apparently, to add this to your security @EnableReactiveMethodSecurity annotation:

    @EnableReactiveMethodSecurity(proxyTargetClass = true)
    

    Without the proxyTargetClass = true parameter, stuff breaks in weird ways.