Reputation: 149
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.
Upvotes: 5
Views: 8566
Reputation: 153
Spent sometime to find this 404 was due to the presence of both libraries. The project could work in SpringBoot 2.7.0 but not in 3.0.5.
Upvotes: 0
Reputation: 1454
To all those facing the same issue, but have noticed that your issue does not involve contentType
mismatching, I recommend checking your dependencies (build.gradle, pom.xml, etc.).
In my case, I encountered the same problem when I had both of the following dependencies defined:
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
If you're using the functional style for declaring your endpoints (using RouterFunction
), having both dependencies can cause issues.
To resolve this, you should remove one of the dependencies. If you are working with spring-webflux
, make sure to keep only the following in your build configuration:
implementation 'org.springframework.boot:spring-boot-starter-webflux'
Note: It's important to note that as of spring-boot 3.1.0
, there are no warnings or errors when both of the dependencies are present. You just receive 404 Not Found
response without any further indication of the problem.
You can find a working small application here in the spring.io guide
Upvotes: 7
Reputation: 151
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)
Upvotes: 4