All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.trendyol.stove.testing.e2e.kafka.KafkaSystem.kt Maven / Gradle / Ivy

There is a newer version: 1f1ca59
Show newest version
package com.trendyol.stove.testing.e2e.kafka

import arrow.core.None
import arrow.core.Option
import arrow.core.Some
import arrow.core.getOrElse
import com.trendyol.stove.functional.Try
import com.trendyol.stove.functional.recover
import com.trendyol.stove.testing.e2e.system.TestSystem
import com.trendyol.stove.testing.e2e.system.abstractions.ExposesConfiguration
import com.trendyol.stove.testing.e2e.system.abstractions.PluggedSystem
import com.trendyol.stove.testing.e2e.system.abstractions.RunnableSystemWithContext
import com.trendyol.stove.testing.e2e.system.abstractions.StateOfSystem
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.future.await
import kotlinx.coroutines.runBlocking
import org.apache.kafka.clients.producer.ProducerRecord
import org.apache.kafka.common.header.internals.RecordHeader
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.getBean
import org.springframework.context.ApplicationContext
import org.springframework.kafka.core.KafkaTemplate
import kotlin.reflect.KClass
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

class KafkaSystem(
    override val testSystem: TestSystem,
    private val context: KafkaContext
) : PluggedSystem, RunnableSystemWithContext, ExposesConfiguration {
    private val logger: Logger = LoggerFactory.getLogger(javaClass)
    private lateinit var applicationContext: ApplicationContext
    private lateinit var kafkaTemplate: KafkaTemplate
    private lateinit var exposedConfiguration: KafkaExposedConfiguration
    val getInterceptor = { applicationContext.getBean(TestSystemKafkaInterceptor::class.java) }
    private val state: StateOfSystem =
        StateOfSystem(testSystem.options, javaClass.kotlin, KafkaExposedConfiguration::class)

    override suspend fun beforeRun() {}

    override suspend fun run() {
        exposedConfiguration = state.capture {
            context.container.start()
            KafkaExposedConfiguration(context.container.bootstrapServers)
        }
    }

    override suspend fun afterRun(context: ApplicationContext) {
        applicationContext = context
        kafkaTemplate = context.getBean()
        kafkaTemplate.setProducerListener(getInterceptor())
    }

    override fun configuration(): List = context.configureExposedConfiguration(exposedConfiguration) + listOf(
        "kafka.bootstrapServers=${exposedConfiguration.bootstrapServers}",
        "kafka.isSecure=false"
    )

    override suspend fun stop(): Unit = context.container.stop()

    override fun close(): Unit = runBlocking {
        Try {
            kafkaTemplate.destroy()
            executeWithReuseCheck { stop() }
        }.recover {
            logger.warn("got an error while closing KafkaSystem", it)
        }
    }

    suspend fun publish(
        topic: String,
        message: Any,
        key: Option = None,
        headers: Map = mapOf(),
        testCase: Option = None
    ): KafkaSystem {
        val record = ProducerRecord(
            topic,
            0,
            key.getOrElse { "" },
            context.objectMapper.writeValueAsString(message),
            headers.toMutableMap().addTestCase(testCase).map { RecordHeader(it.key, it.value.toByteArray()) }
        )
        return kafkaTemplate.usingCompletableFuture().send(record).await().let { this }
    }

    suspend fun shouldBeConsumed(
        atLeastIn: Duration = 5.seconds,
        message: Any
    ): KafkaSystem = coroutineScope {
        shouldBeConsumedInternal(message::class, atLeastIn) { incomingMessage -> incomingMessage == Some(message) }
    }.let { this }

    suspend fun shouldBeFailed(
        atLeastIn: Duration = 5.seconds,
        message: Any,
        exception: Throwable
    ): KafkaSystem = coroutineScope {
        shouldBeFailedInternal(message::class, atLeastIn) { option, throwable -> option == Some(message) && throwable == exception }
    }.let { this }

    @PublishedApi
    internal suspend fun  shouldBeFailedOnCondition(
        atLeastIn: Duration = 5.seconds,
        condition: (T, Throwable) -> Boolean,
        clazz: KClass
    ): KafkaSystem = coroutineScope {
        shouldBeFailedInternal(clazz, atLeastIn) { message, throwable -> message.isSome { m -> condition(m, throwable) } }
    }.let { this }

    private suspend fun  shouldBeFailedInternal(
        clazz: KClass,
        atLeastIn: Duration,
        condition: (Option, Throwable) -> Boolean
    ): Unit = coroutineScope { getInterceptor().waitUntilFailed(atLeastIn, clazz, condition) }

    @PublishedApi
    internal suspend fun  shouldBeConsumedOnCondition(
        atLeastIn: Duration = 5.seconds,
        condition: (T) -> Boolean,
        clazz: KClass
    ): KafkaSystem = coroutineScope {
        shouldBeConsumedInternal(clazz, atLeastIn) { incomingMessage -> incomingMessage.isSome { o -> condition(o) } }
    }.let { this }

    private suspend fun  shouldBeConsumedInternal(
        clazz: KClass,
        atLeastIn: Duration,
        condition: (Option) -> Boolean
    ): Unit = coroutineScope { getInterceptor().waitUntilConsumed(atLeastIn, clazz, condition) }

    suspend fun shouldBePublished(
        atLeastIn: Duration = 5.seconds,
        message: Any
    ): KafkaSystem = coroutineScope {
        shouldBePublishedInternal(message::class, atLeastIn) { incomingMessage -> incomingMessage == Some(message) }
    }.let { this }

    @PublishedApi
    internal suspend fun  shouldBePublishedOnCondition(
        atLeastIn: Duration = 5.seconds,
        condition: (T) -> Boolean,
        clazz: KClass
    ): KafkaSystem = coroutineScope {
        shouldBePublishedInternal(clazz, atLeastIn) { incomingMessage -> incomingMessage.isSome { o -> condition(o) } }
    }.let { this }

    private suspend fun  shouldBePublishedInternal(
        clazz: KClass,
        atLeastIn: Duration,
        condition: (Option) -> Boolean
    ): Unit = coroutineScope { getInterceptor().waitUntilPublished(atLeastIn, clazz, condition) }

    companion object {

        /**
         * Extension for [KafkaSystem.shouldBeConsumedOnCondition] to enable generic invocation as method<[T]>(...)
         */
        suspend inline fun  KafkaSystem.shouldBeConsumedOnCondition(
            atLeastIn: Duration = 5.seconds,
            noinline condition: (T) -> Boolean
        ): KafkaSystem = this.shouldBeConsumedOnCondition(atLeastIn, condition, T::class)

        /**
         * Extension for [KafkaSystem.shouldBeFailedOnCondition] to enable generic invocation as method<[T]>(...)
         */
        suspend inline fun  KafkaSystem.shouldBeFailedOnCondition(
            atLeastIn: Duration = 5.seconds,
            noinline condition: (T, Throwable) -> Boolean
        ): KafkaSystem = this.shouldBeFailedOnCondition(atLeastIn, condition, T::class)

        /**
         * Extension for [KafkaSystem.shouldBePublishedOnCondition] to enable generic invocation as method<[T]>(...)
         */
        suspend inline fun  KafkaSystem.shouldBePublishedOnCondition(
            atLeastIn: Duration = 5.seconds,
            noinline condition: (T) -> Boolean
        ): KafkaSystem = this.shouldBePublishedOnCondition(atLeastIn, condition, T::class)
    }
}

private fun (MutableMap).addTestCase(testCase: Option): MutableMap =
    if (this.containsKey("testCase")) {
        this
    } else {
        testCase.map { this["testCase"] = it }.let { this }
    }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy