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.
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
I`m trying to run a simple spring batch job that read xml in kotlin but i got the error :
...
Caused by: javax.xml.stream.XMLStreamException: ParseError at [row,col]:[3,1]
Message: Content is not allowed in prolog.
XML Prolog is OK
const val test = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root><test>test</test></root>"
#XmlAccessorType(XmlAccessType.FIELD)
#XmlRootElement(name = "test")
class Test(
#XmlValue
var test: String = ""
)
#Configuration
#EnableBatchProcessing
class BatchStgCarbPdv(
#Autowired
val jobBuilderFactory: JobBuilderFactory,
#Autowired
val stepBuilderFactory: StepBuilderFactory
) {
I can't find the error in the reader
fun read(): StaxEventItemReader<Test> {
val reader: StaxEventItemReader<Test> = StaxEventItemReader()
println(StandardCharsets.UTF_8.name())
reader.setEncoding(StandardCharsets.UTF_8.name())
reader.setResource(ByteArrayResource(test.encodeToByteArray()))
val unmarshaller = Jaxb2Marshaller()
unmarshaller.setClassesToBeBound(Test::class.java)
reader.setUnmarshaller(unmarshaller)
reader.setFragmentRootElementName("root")
reader.afterPropertiesSet()
return reader
}
Spring batch...
fun processor(): com.gs.ItemProcessor {
return ItemProcessor()
}
fun writer(): ConsoleItemWriter {
return ConsoleItemWriter()
}
#Bean
fun step1(): Step {
return stepBuilderFactory["step1"]
.chunk<Test, Test>(1)
.reader(read())
.processor(processor())
.writer(writer())
.taskExecutor(SimpleAsyncTaskExecutor("spring_batch"))
.build()
}
#Bean
fun importUserJob(step1: Step): Job {
return jobBuilderFactory["importJob"]
.incrementer(RunIdIncrementer())
.flow(step1)
.end()
.build()
}
}
Do nothing
class ItemProcessor : ItemProcessor<Test, Test> {
override fun process(t: Test): Test {
return t
}
}
class ConsoleItemWriter : ItemWriter<Test?> {
override fun write(items: List<Test?>) {
for (test in items) {
println(test)
}
}
}
For starters i want to secure a part of my rest-api with basic auth.
When I try to access endpoints from a react client, I keep getting 401's in the preflight requests.
I tried to follow this guide without success:
https://www.baeldung.com/spring-security-cors-preflight
i'm not sure if this is part of the problem, but another part can only be accessed with certain custom http headers.
I'm using Method Security:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = false)
class MethodSecurityConfig : GlobalMethodSecurityConfiguration() {
override fun customMethodSecurityMetadataSource(): MethodSecurityMetadataSource = SecurityMetadataSource()
override fun accessDecisionManager(): AccessDecisionManager = super.accessDecisionManager().apply {
this as AbstractAccessDecisionManager
decisionVoters.add(PrivilegeVoter())
}
}
And this is my Security config:
#Configuration
#EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class SecurityConfig : WebSecurityConfigurerAdapter() {
private val deviceRequestHeaderName: String = "X-DEVICE-ID"
private val platformRequestHeaderName: String = "X-PLATFORM-ID"
#Autowired
lateinit var users: AppUserRepository
#Autowired
lateinit var backendUsers: BackendUserRepository
#Autowired
lateinit var roles: RoleRepository
val authManager by lazy { authenticationManager() }
private val authProvider by lazy {
PreAuthenticatedAuthenticationProvider().apply {
setPreAuthenticatedUserDetailsService {
val authId = it.principal as UserAuthId
if (authId.deviceId == null) throw UsernameNotFoundException("No device-id to search for.")
if (authId.platform == null) throw UsernameNotFoundException("Platform not specified.")
val platform = try {
ApplicationPlatform.valueOf(authId.platform)
} catch (e: IllegalArgumentException) {
throw UsernameNotFoundException("Unknown platform ${authId.platform}.")
}
val existingUser = users.findByUserDeviceIdAndPlatform(authId.deviceId, platform)
if (existingUser != null) return#setPreAuthenticatedUserDetailsService existingUser
users.save(AppUser(authId.deviceId, platform, roles))
}
}
}
val passwordEncoder by lazy { BCryptPasswordEncoder() }
private val deviceIdFilter by lazy {
HeaderFieldAuthFilter(deviceRequestHeaderName, platformRequestHeaderName).apply {
setAuthenticationManager(authManager)
}
}
override fun configure(auth: AuthenticationManagerBuilder) = auth {
authenticationProvider(authProvider)
val userDetailsService = BackendUserDetailsService(backendUsers)
userDetailsService(userDetailsService).passwordEncoder(passwordEncoder)
}
override fun configure(http: HttpSecurity) = http {
session {
sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
exceptionHandling()
addFilter(deviceIdFilter)
authorizeRequests().anyRequest().authenticated()
csrf().disable()
httpBasic()
cors().configurationSource { request ->
CorsConfiguration().apply {
allowedOrigins = listOf(ALL)
allowedMethods = listOf(GET, POST, DELETE, PUT, OPTIONS).map { it.name }
allowedHeaders = listOf(ALL)
allowCredentials = true
maxAge = 3600
}
}
}
#Bean
fun auditorProvider(): AuditorAware<User> = AuditorAware<User> {
val authentication = SecurityContextHolder.getContext().authentication
val user = authentication.run { if (isAuthenticated) principal as? User else null }
return#AuditorAware Optional.ofNullable(user)
}
}
I could solve by manually exclude the preflight requests from authentication.
adding this
antMatchers(OPTIONS, "/**").permitAll()
to the authorizeRequests() configuration accomplishes that.
Note that Options is a direct reference to the HttpMethod enum value, imported like this
import org.springframework.http.HttpMethod.*
Stackoverflow posts that helped me get there:
Response for preflight has invalid HTTP status code 401 - Spring
How to configure CORS in a Spring Boot + Spring Security application?
Originally I had assumed, that this should have been handled by the cors configuration - which it was apparently not.
To enable CORS for a single rest endpoint you can annotate it with:
#CrossOrigin
To allow for CORS for all endpoints you can have a bean like so:
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST", "PUT", "DELETE").allowedOrigins("*")
.allowedHeaders("*");
}
};
I have two kinds of Webflux applications, annotation-based and route-based. These applications are called with a set of headers, some of which (Open Tracing) I need to propagate in downstream calls using WebClient.
If these were normal Spring WebMvc applications I would use a Filter to keep the selected headers in a ThreadLocal, access it in a RestTemplate interceptor to send them to subsequent services and clear the ThreadLocal.
What's the proper way to replicate this behaviour in WebFlux applications?
I solved it using Project Reactor's Context to store the headers in a WebFilter. Then they are gotten in the WebClient's ExchangeFilterFunction. Here's the whole solution:
WebFilter
class OpenTracingFilter(private val openTracingHeaders: Set<String>) : WebFilter {
private val logger = LoggerFactory.getLogger(javaClass)
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
return chain.filter(exchange)
.subscriberContext { ctx ->
var updatedContext = ctx
exchange.request.headers.forEach {
if (openTracingHeaders.contains(it.key.toLowerCase())) {
logger.debug("Found OpenTracing Header - key {} - value {}", it.key, it.value[0])
updatedContext = updatedContext.put(it.key, it.value[0])
}
}
updatedContext
}
}
}
OpenTracingExchangeFilterFunction
class OpenTracingExchangeFilterFunction(private val headers: Set<String>) : ExchangeFilterFunction {
private val logger = LoggerFactory.getLogger(javaClass)
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
logger.debug("OpenTracingExchangeFilterFunction - filter()")
return OpenTracingClientResponseMono(request, next, headers)
}
}
OpenTracingClientResponseMono
class OpenTracingClientResponseMono(private val request: ClientRequest,
private val next: ExchangeFunction,
private val headersToPropagate: Set<String>) : Mono<ClientResponse>() {
private val logger = LoggerFactory.getLogger(javaClass)
override fun subscribe(subscriber: CoreSubscriber<in ClientResponse>) {
val context = subscriber.currentContext()
val requestBuilder = ClientRequest.from(request)
requestBuilder.headers { httpHeaders ->
headersToPropagate.forEach {
if(context.hasKey(it)) {
logger.debug("Propagating header key {} - value{}", it, context.get<String>(it))
httpHeaders[it] = context.get<String>(it)
}
}
}
val mutatedRequest = requestBuilder.build()
next.exchange(mutatedRequest).subscribe(subscriber)
}
}
OpenTracingConfiguration
#Configuration
class OpenTracingConfiguration(private val openTracingConfigurationProperties: OpenTracingConfigurationProperties) {
#Bean
fun webClient(): WebClient {
return WebClient.builder().filter(openTracingExchangeFilterFunction()).build()
}
#Bean
fun openTracingFilter(): WebFilter {
return OpenTracingFilter(openTracingConfigurationProperties.headers)
}
#Bean
fun openTracingExchangeFilterFunction(): OpenTracingExchangeFilterFunction {
return OpenTracingExchangeFilterFunction(openTracingConfigurationProperties.headers)
}
}
OpenTracingConfigurationProperties
#Configuration
#ConfigurationProperties("opentracing")
class OpenTracingConfigurationProperties {
lateinit var headers: Set<String>
}
application.yml
opentracing:
headers:
- x-request-id
- x-b3-traceid
- x-b3-spanid
- x-b3-parentspanid
- x-b3-sampled
- x-b3-flags
- x-ot-span-context
I needed to pass x-request-id header to a downstream service in my application. Achieved this by adding WebFilter that writes x-request-id to a reactor context
class ContextWebFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val headers = exchange.request.headers
val xRequestId = headers[X_REQUEST_ID]?.firstOrNull() ?: ""
val requestId = xRequestId.ifBlank { UUID.randomUUID().toString() }
return chain
.filter(exchange)
.contextWrite { it.put(X_REQUEST_ID, requestId) }
}
companion object {
const val X_REQUEST_ID = "X-REQUEST-ID"
}
}
and updating WebClient with ExchangeFilterFunction that updates outgoing request
WebClient.builder()
.filter(
ExchangeFilterFunction.ofRequestProcessor { request ->
Mono.deferContextual { context ->
val xRId = context.getOrDefault<String>("X-REQUEST-ID", "")
logger.debug("Set X-REQUEST-ID={} as a header to outgoing call", xRId)
Mono.just(
ClientRequest.from(request)
.header("X-REQUEST-ID", xRId)
.build()
)
}
}
)
.baseUrl("http://localhost:8080")
.build()
Let's say I have this router definition:
#Component
class PersonRouter(private val handler: PersonHandler) {
#Bean
fun router(): RouterFunction<ServerResponse> = router {
("/api/people" and accept(MediaType.APPLICATION_JSON_UTF8)).nest {
GET("/{id}") { handler.findById(it) }
}
}
And then this handler:
#Component
#Transactional
class PersonHandler(private val repository: PersonRepository) {
private companion object : KLogging()
#Transactional(readOnly = true)
fun findById(req: ServerRequest): Mono<ServerResponse> {
logger.info { "${req.method()} ${req.path()}" }
val uuid = ? // req.pathContainer().elements().last().value()
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(repository.findById(uuid)))
.switchIfEmpty(ServerResponse.notFound().build())
}
}
How do I access the identifier (what would be a #PathVariable id: String on a typical #RestController) from ServerRequest without doing black magic with regular expressions, string-heavy-lifting, and such things?
Ah! Found it!
It is by doing: req.pathVariable("id")
It was there all the time...in the official Spring Framework (Web Reactive) documentation!
I have the following question. I want to save a user into a mongo db via reactive spring data repositories only if the e-mail of the user is not already present.
#Component("userService")
class UserService(private val repository: UserRepository){
fun checkIfEMailExists(email: String): Mono<Boolean> {
return repository.findByEMail(email).hasElement()
}
fun create(user: User): Mono<User> {
//not sure how to do this bit here
this.checkIfEMailExists(user.email)
.filter{ it -> it == true}
.map{repository.save(user)}
}
}
So, basically I am not sure how i can handle the Mono of boolean to do something only if it's value is true (and otherwise thrown an exception)
First, your MongoDB repository should be a reactive one and return a Mono<User> or Flux<User> for that kind of signature:
public interface UserRepository extends ReactiveMongoRepository<User, String> {
Mono<User> findByEmail(String email);
}
Then you can chain that reactive type with other operators like this:
#Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public Mono<User> create(User user) {
return this.userRepository.findByEmail(user.getEmail())
.flatMap(existingUser -> Mono.error(new UserAlreadyPresentException(existingUser.getEmail())))
.then(this.userRepository.save(user));
}
class UserAlreadyPresentException extends RuntimeException {
public UserAlreadyPresentException(String email) {
super("User already present with email " + email);
}
}
}