Spring WebFlux Route always returns 404

Viewed 5340

I am working on a simple project which uses Spring Boot 2 with Spring WebFlux using Kotlin. I wrote test for my handler function (in which I mock the dependencies using Mockito).

However, it seems like my route function does not trigger the handler, as all of my requests return HTTP 404 NOT FOUND (even though the route is correct).

I have looked at various other projects to find out what how these tests are supposed to be written (here, here), but the problem persists.

The code is as follows (and can also be found on GitHub):

UserRouterTest

@ExtendWith(SpringExtension::class, MockitoExtension::class)
@Import(UserHandler::class)
@WebFluxTest
class UserRouterTest {

    @MockBean
    private lateinit var userService: UserService

    @Autowired
    private lateinit var userHandler: UserHandler

    @Test
    fun givenExistingCustomer_whenGetCustomerByID_thenCustomerFound() {
        val expectedCustomer = User("test", "test")
        val id = expectedCustomer.userID

        `when`(userService.getUserByID(id)).thenReturn(Optional.ofNullable(expectedCustomer))

        val router = UserRouter().userRoutes(userHandler)
        val client = WebTestClient.bindToRouterFunction(router).build()

        client.get()
                .uri("/users/$id")
                .accept(MediaType.ALL)
                .exchange()
                .expectStatus().isOk
                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
                .expectBody(User::class.java)
    }
}

User

@Entity
class User(var username : String, var password: String) {

    @Id
    val userID = UUID.randomUUID()
}

UserRepository

@Repository
interface UserRepository : JpaRepository<User, UUID>{
}

UserService

@Service
class UserService(
        private val userRepository: UserRepository
) {
    fun getUserByID(id: UUID): Optional<User> {
        return Optional.of(
                try {
                    userRepository.getOne(id)
                } catch (e: EntityNotFoundException) {
                    User("test", "test")
                }
        )
    }

    fun addUser(user: User) {
        userRepository.save(user)
    }
}

UserHandler

@Component
class UserHandler(
        private val userService: UserService
) {
    fun getUserWithID(request: ServerRequest): Mono<ServerResponse> {
        val id = try {
            UUID.fromString(request.pathVariable("userID"))
        } catch (e: IllegalArgumentException) {
            return ServerResponse.badRequest().syncBody("Invalid user id")
        }
        val user = userService.getUserByID(id).get()
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(BodyInserters.fromObject(user))
    }
}

UserRouter

@Configuration
class UserRouter {
    @Bean
    fun userRoutes(userHandler: UserHandler) = router {
        contentType(MediaType.APPLICATION_JSON_UTF8).nest {
            GET("/users/{userID}", userHandler::getUserWithID)
            GET("") { ServerResponse.ok().build() }
        }
    }
}

EDIT

To route based on the presence of one or more query parameter (regardless of their values), we can do the following: UserRouter

@Configuration
class UserRouter {
    @Bean
    fun userRoutes(userHandler: UserHandler) = router {

        GET("/users/{userID}", userHandler::getUserWithID)
        (GET("/users/")
                and queryParam("username") { true }
                and queryParam("password") { true }
                )
                .invoke(userHandler::getUsers)
    }
}

Note that GET("/users/?username={username}", userHandler::getUsersWithUsername) does not work.

1 Answers

The way the router is configured - contentType(MediaType.APPLICATION_JSON_UTF8).nest - will only match requests that have this content type, so you would have to either remove the contentType prerequisite or change the test to include it

client.get()
                .uri("/users/$id")
                .accept(MediaType.ALL)
                .header("Content-Type", "application/json;charset=UTF-8")
                .exchange()
                .expectStatus().isOk
                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)
                .expectBody(User::class.java)
Related