I'm super frustrated with a Kotlin/Mockito problem
What I want to accomplish is very simple, I've an AuthorizationFilter on my springboot application and for test purposes I want to mock its behavior to let the test requests pass by
My AuthorizationFilter indeed calls an API which will then provide the user auth status. so I thought that the simplest way to mock this is just have the AuthApi mocked into the filter and return whatever status I want... BUT IT WORKS RANDOMLY
#Component
class AuthorizationFilter(
val authApi: authApi
) : OncePerRequestFilter() {
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
if (request.method.equals("OPTIONS")) {
filterChain.doFilter(request, response)
return
}
val token = request.getHeader("authorization")
if (token == null) {
response.sendError(401)
return
}
runCatching {
authApi.authorize(token.replace("Bearer ", ""))
}.onSuccess {
AuthorizationContext.set(it)
filterChain.doFilter(request, response)
}.onFailure {
it.printStackTrace()
response.sendError(401)
}
}
}
the authApi authorize method is irrelevant to this question, but just let it be clear it will NEVER return null... it might throw an exception but wont return null
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
#ExtendWith(SpringExtension::class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#ActiveProfiles("test")
class SocketIOServerTest {
#MockBean
lateinit var mockedApiComponent: AuthApi
#Autowired
lateinit var boardRepository: BoardRepository
#Autowired
private lateinit var servletRegistrationBean: ServletRegistrationBean<SocketIOServer>
private lateinit var socketIOServer: SocketIOServer
#LocalServerPort
private val serverPort: String? = null
lateinit var clientSocket: Socket
private val userId = 1
private val groupId = 123
private val admin = false
private val auth = Authorization("token", userId, groupId, admin)
private val objectMapper = ObjectMapper()
#BeforeAll
fun connect() {
AuthorizationContext.set(auth)
Mockito.`when`(mockedApiComponent.authorize(anyOrNull())).thenReturn(auth)
socketIOServer = servletRegistrationBean.servlet
clientSocket = IO.socket("http://localhost:${serverPort}", IO.Options.builder().setExtraHeaders(mutableMapOf(Pair("Authorization", listOf("Bearer token")))).build())
clientSocket.on(Socket.EVENT_CONNECT) {
println("client connected")
}
clientSocket.on(Socket.EVENT_DISCONNECT) {
println("client disconnected")
}
clientSocket.connect()
}
#Test
fun testPingPong() {
var finished = false
clientSocket.on("pong") {
println("event: ${it[0]}")
val pongTime = (it[0] as String).substring(18, 31).toLong()
assertTrue(System.currentTimeMillis() - pongTime < 1000)
finished = true
}
clientSocket.emit("ping")
while (!finished) Thread.yield()
}
#Test
fun testBasicNotification(){
clientSocket.on("basic_notification"){
println(Arrays.toString(it))
}
socketIOServer.send(SocketIOEvent("${groupId}","basic_notification","data"))
Thread.sleep(1000)
}
#Test
fun testBoardNotification() {
clientSocket.on("entity_create") {
val event = it[0] as String
println("event: $event")
val eventValue = objectMapper.readValue(event, Map::class.java)
val entityValue = eventValue["entity"] as Map<*, *>
assertEquals("BOARD", eventValue["entity_type"])
assertEquals("board name", entityValue["name"])
assertEquals(groupId, entityValue["groupId"])
assertEquals(userId, entityValue["created_by"])
assertEquals(userId, entityValue["last_modified_by"])
}
val board = boardRepository.save(Board(groupId, "board name"))
//boardRepository.delete(board)
}}
Just to be clear, THE TEST WORKS, the assertions are correct and although it has some random behavior at the end it works.... BUT IT PRINTS A BIG STACK TRACE DUE SOME CRAZY BEHAVIOR
As you can see I'm using a SocketIO client which sends several requests out of my code... some of them get authenticated and some of them throw nullpointerexception on this line
.onSuccess {
AuthorizationContext.set(it) //this line
filterChain.doFilter(request, response)
}.
because it is null, because somehow the mockedApiComponent.authorize() returned null... again which would be impossible on the real component and which shouldn't be happening because the mock clearly states which object to return
I've exhaustively debbuged this code, thinking that somehow junit got two beans of the AuthApi
but the whole execution shows the same object id which matches the mock... and even weirder that the token parameter used on authorize is always the same
can anyone help me?
I've exhaustively debbuged this code, thinking that somehow junit got two beans of the AuthApi but the whole execution shows the same object id which matches the mock... and even weirder that the token parameter used on authorize is always the same
This looks to me disturbing, like some problem with async code at runtime. I would try to do a couple of things:
Check for when the context is null in: AuthorizationContext.set(it) and put some more debug code to know what is happening. Or just debug from there
Use a recover{} block to deal with the NullPointerException and debug from there to see original problem in stack trace
What happens when instead runCatching{} you use mapCatching{}?
Related
So I want to create a simple food order service, but this service require some information from the other service so I use saga pattern. Here's how it should work if I order a food, first it will attempt to create order but if there's any error it will retry for 3 times and publish either success or failed event.
Here's the sample code.
#Saga
class OrderCreationSaga {
#Transient
#Autowired
private lateinit var commandGateway: CommandGateway
#Transient
#Autowired
private lateinit var eventScheduler: EventScheduler
#Transient
#Autowired
private lateinit var eventBus: EventBus
#Transient
#Autowired
private lateinit var scheduleToken: ScheduleToken
private lateinit var orderId: String
private var retryCounter = 1
#StartSaga
#SagaEventHandler(associationProperty = "orderId")
fun on(event: OrderCreationAttempted) {
this.orderId = event.orderId
eventBus.publish(GenericEventMessage(event.toOrderCreationRequested()))
}
#SagaEventHandler(associationProperty = "orderId")
fun on(event: OrderCreationRequested) {
try {
// send data to another service
orderCreationService.createOrder(event).block()
eventBus.publish(GenericEventMessage(
OrderCreationSuccess(
orderId = event.orderId
))
)
} catch (error: Throwable) {
// catching request error, retry for 3 times
if (this.retryCounter == 3) {
eventBus.publish(GenericEventMessage(
OrderCreationFailed(
orderId = this.orderId,
)
))
} else {
eventBus.publish(GenericEventMessage(
OrderCreationRetry(
orderId = event.orderId,
)
))
this.retryCounter++
}
}
}
#EndSaga
#SagaEventHandler(associationProperty = "orderId")
fun on(event: OrderCreationSuccess) {
// do the success job
}
#EndSaga
#SagaEventHandler(associationProperty = "orderId")
fun on(event: OrderCreationFailed) {
// do the failed job
}
#SagaEventHandler(associationProperty = "orderId")
fun on(event: OrderCreationRetry) {
val duration = Duration.ofSeconds(30)
val scheduleEvent = OrderCreationRequested(orderId = event.orderId)
scheduleToken = eventScheduler.schedule(duration, scheduleEvent)
}
}
But the weird thing happens so after it published a success event it will publish a OrderCreationRequested event again for some reason (I know this because I've checked the event log inside axonserver). This keeps looping infinitely, is this because my code or some configuration or could be something else?
So the problem was I forgot to set my username and password for my MongoDB and then someone just trying to delete all of my data including tracking token for axon-server. So because of the tracking token has been delete axon-server start creating the new one with 0 value which makes all the event start rerunning again and again. I solve this problem by just add the username and password for my MongoDB.
i was wondering if it is possible to send messages from the backend (for example a running task that receives information from an external system) to the UI. In my case it needs to be a specific session (no broadcast) and only on a specific screen
plan B would be polling the backend frequently but i was hoping to get something more "realtime"
I was trying to work something out like this, but i keep getting a NotSerializableException.
#Push
class StorageAccess : Screen(), MessageListener {
#Inject
private lateinit var stationWSService: StationWebSocketService
#Inject
private lateinit var notifications: Notifications
#Subscribe
private fun onInit(event: InitEvent) {
}
#Subscribe("stationPicker")
private fun onStationPickerValueChange(event: HasValue.ValueChangeEvent<StorageUnit>) {
val current = AppUI.getCurrent()
current.userSession.id ?: return
val prevValue = event.prevValue
if (prevValue != null) {
stationWSService.remove(current.userSession.id)
}
val value = event.value ?: return
stationWSService.listen(current.userSession.id, value, this)
}
override fun messageReceived(message: String) {
val current = AppUI.getCurrent()
current.access {
notifications.create().withCaption(message).show()
}
}
#Subscribe
private fun onAfterDetach(event: AfterDetachEvent) {
val current = AppUI.getCurrent()
current.userSession.id ?: return
stationWSService.remove(current.userSession.id)
}
}
-- The callback interface
interface MessageListener : Serializable {
fun messageReceived(message: String);
}
-- The listen method of my backend service
private val listeners: MutableMap<String, MutableMap<UUID, MessageListener>> = HashMap()
override fun listen(id: UUID, storageUnit: StorageUnit, callback: MessageListener) {
val unitStationIP: String = storageUnit.unitStationIP ?: return
if (!listeners.containsKey(unitStationIP))
listeners[unitStationIP] = HashMap()
listeners[unitStationIP]?.set(id, callback)
}
The Exception i get is NotSerializableException: com.haulmont.cuba.web.sys.WebNotifications which happens during adding the listener to the backend: stationWSService.listen(current.userSession.id, value, this)
as far as i understand this is the place where the UI sends the information to the backend - and with it the entire status of the class StorageAccess, including all its members.
is there an elegant solution to this?
regards
There is an add-on that solves exactly this problem: https://github.com/cuba-platform/global-events-addon
Currently, the ktor client logging implementation is as below, and it works as intended but not what I wanted to have.
public class Logging(
public val logger: Logger,
public var level: LogLevel,
public var filters: List<(HttpRequestBuilder) -> Boolean> = emptyList()
)
....
private suspend fun logRequest(request: HttpRequestBuilder): OutgoingContent? {
if (level.info) {
logger.log("REQUEST: ${Url(request.url)}")
logger.log("METHOD: ${request.method}")
}
val content = request.body as OutgoingContent
if (level.headers) {
logger.log("COMMON HEADERS")
logHeaders(request.headers.entries())
logger.log("CONTENT HEADERS")
logHeaders(content.headers.entries())
}
return if (level.body) {
logRequestBody(content)
} else null
}
Above creates a nightmare while looking at the logs because it's logging in each line. Since I'm a beginner in Kotlin and Ktor, I'd love to know the way to change the behaviour of this. Since in Kotlin, all classes are final unless opened specifically, I don't know how to approach on modifying the logRequest function behaviour. What I ideally wanted to achieve is something like below for an example.
....
private suspend fun logRequest(request: HttpRequestBuilder): OutgoingContent? {
...
if (level.body) {
val content = request.body as OutgoingContent
return logger.log(value("url", Url(request.url)),
value("method", request.method),
value("body", content))
}
Any help would be appreciative
No way to actually override a private method in a non-open class, but if you just want your logging to work differently, you're better off with a custom interceptor of the same stage in the pipeline:
val client = HttpClient(CIO) {
install("RequestLogging") {
sendPipeline.intercept(HttpSendPipeline.Monitoring) {
logger.info(
"Request: {} {} {} {}",
context.method,
Url(context.url),
context.headers.entries(),
context.body
)
}
}
}
runBlocking {
client.get<String>("https://google.com")
}
This will produce the logging you want. Of course, to properly log POST you will need to do some extra work.
Maybe this will be useful for someone:
HttpClient() {
install("RequestLogging") {
responsePipeline.intercept(HttpResponsePipeline.After) {
val request = context.request
val response = context.response
kermit.d(tag = "Network") {
"${request.method} ${request.url} ${response.status}"
}
GlobalScope.launch(Dispatchers.Unconfined) {
val responseBody =
response.content.tryReadText(response.contentType()?.charset() ?: Charsets.UTF_8)
?: "[response body omitted]"
kermit.d(tag = "Network") {
"${request.method} ${request.url} ${response.status}\nBODY START" +
"\n$responseBody" +
"\nBODY END"
}
}
}
}
}
You also need to add a method from the Ktor Logger.kt class to your calss with HttpClient:
internal suspend inline fun ByteReadChannel.tryReadText(charset: Charset): String? = try {
readRemaining().readText(charset = charset)
} catch (cause: Throwable) {
null
}
Say I have this situation:
interface Repository {
fun getMovies(success: (List<String>) -> Unit, failure: (Int) -> Unit)
}
and I want to mock the implementation of this interface. Basically in this case, there are two lambdas as input parameters to the getmovie method, and for the test case, I only want to produce success (success.invoke(theMoviesList) should be called).
Below is something similar to what I would like to do:
class MovieViewModel constructor(val repository: AppRepository) {
var movieNames = listOf<String>() // Not private, or live data, for simplicity
fun fetchMovies() {
repository.fetchMovies(
success = {
movies ->
this.movieNames = movies
}}, failure: {
statusCode ->
})
}
}
class MoviePageTests {
private var movieViewModel: MovieViewModel? = null
#Mock
lateinit var mockRepository: AppRepository
#Before
#Throws(Exception::class)
fun before() {
MockitoAnnotations.initMocks(this)
movieViewModel = MovieViewModel(repository = mockRepository)
}
#Test
fun checkFetchMoviesUpdatesMoviesData() {
var testMovies = listof("Dracula", "Superman")
//Set up mockito so that the repository generates success with testMovies above
?????
//
movieViewModel.fetchMovies()
assertEquals(movieViewModel.movies, testMovies)
}
}
I know how to do this by way of a RepositoryImpl, but not in Mockito, despite looking at many examples online.
Any ideas?
I have an oauth2 server and client. In client I configured ClientRegistrationRepository:
#Bean
#Conditional(SsoCondition::class)
open fun clientRegistrationRepository(): ClientRegistrationRepository {
val test = ClientRegistration.withRegistrationId(registrationId)
.clientId(clientId)
.clientSecret(clientSecret)
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")
.authorizationUri(authorizeUri)
.tokenUri(tokenUri)
.userInfoUri(userInfoUri)
.scope("read", "write")
.userNameAttributeName(userNameAttribute)
.build()
return InMemoryClientRegistrationRepository(test)
}
This works fine and authorization is performed.
The problem is in userInfoUri. This uri is invoked and needed server method is performed. I see the user data and that method return this data.
The method for authorizeUri is:
#GetMapping("/api/user/me")
fun getUserInfo(response: HttpServletResponse, request: HttpServletRequest, principal: Principal): HashMap<String, Any?> {
val authentication = SecurityContextHolder.getContext().authentication
val userData = HashMap<String, Any?>()
userData[OUTER_ID] = principal.name
val ssoUser = authentication.userAuthentication.principal.attributes
// getting data from ssoUser to userData
...
return userData
}
And so the question is: where or how can I get this data in the client application?
I don't know how right this solution, but I got the user data like this:
Creating custom implementation of OAuth2AuthorizedClientService interface:
class CustomOAuth2AuthorizedClientService(private val clientRegistrationRepository: ClientRegistrationRepository) : OAuth2AuthorizedClientService {
private val principalData = ConcurrentHashMap<String, Authentication>()
...
override fun saveAuthorizedClient(authorizedClient: OAuth2AuthorizedClient, principal: Authentication) {
...
val key = ... // create some key
principalData[key] = principal
}
...
fun getPrincipal(key: String): Authentication? {
return authorizedClientsPrincipal[key]
}
}
Creating bean for CustomOAuth2AuthorizedClientService:
#Bean
open fun authorizedClientService(): OAuth2AuthorizedClientService {
return CustomOAuth2AuthorizedClientService(clientRegistrationRepository())
}
where clientRegistrationRepository() is a ClientRegistrationRepository bean.
In the code get user data from CustomOAuth2AuthorizedClientService bean:
#Autowired
private var oAuth2AuthorizedClientService: OAuth2AuthorizedClientService
...
fun test() {
val userData = (oAuth2AuthorizedClientService as CustomOAuth2AuthorizedClientService).getPrincipal(key)
}