move subscribeToBus in configureGameListener function, and use it on test

This commit is contained in:
2025-03-30 03:08:36 +02:00
parent d5d6a48df3
commit 2fb4c778fd
10 changed files with 104 additions and 37 deletions

View File

@@ -19,8 +19,6 @@ import redis.clients.jedis.UnifiedJedis
*/ */
class GameStateRepositoryInRedis( class GameStateRepositoryInRedis(
eventStore: GameEventStore, eventStore: GameEventStore,
projectionBus: GameProjectionBus,
eventBus: GameEventBus,
jedis: UnifiedJedis, jedis: UnifiedJedis,
snapshotConfig: SnapshotConfig = SnapshotConfig(), snapshotConfig: SnapshotConfig = SnapshotConfig(),
) : GameStateRepository { ) : GameStateRepository {
@@ -36,7 +34,10 @@ class GameStateRepositoryInRedis(
jedis = jedis, jedis = jedis,
) )
init { fun subscribeToBus(
projectionBus: GameProjectionBus,
eventBus: GameEventBus,
) {
// On new event was received, build snapshot and publish it to the projection bus // On new event was received, build snapshot and publish it to the projection bus
eventBus.subscribe { event -> eventBus.subscribe { event ->
withLoggingContext("event" to event.toString()) { withLoggingContext("event" to event.toString()) {

View File

@@ -12,9 +12,7 @@ import io.github.oshai.kotlinlogging.withLoggingContext
import java.util.concurrent.ConcurrentSkipListSet import java.util.concurrent.ConcurrentSkipListSet
class ReactionListener( class ReactionListener(
private val projectionBus: GameProjectionBus,
private val eventHandler: GameEventHandler, private val eventHandler: GameEventHandler,
private val priority: Int = DEFAULT_PRIORITY,
) { ) {
companion object Config { companion object Config {
const val DEFAULT_PRIORITY = -1000 const val DEFAULT_PRIORITY = -1000
@@ -23,7 +21,10 @@ class ReactionListener(
private val logger = KotlinLogging.logger { } private val logger = KotlinLogging.logger { }
fun init() { fun subscribeToBus(
projectionBus: GameProjectionBus,
priority: Int = DEFAULT_PRIORITY,
) {
if (registeredListeners.add(projectionBus)) { if (registeredListeners.add(projectionBus)) {
projectionBus.subscribe(priority) { projection: Projection<GameId> -> projectionBus.subscribe(priority) { projection: Projection<GameId> ->
if (projection !is GameState) return@subscribe if (projection !is GameState) return@subscribe

View File

@@ -1,10 +1,17 @@
package eventDemo.configuration.business package eventDemo.configuration.business
import eventDemo.adapter.infrastructureLayer.event.projection.GameListRepositoryInRedis
import eventDemo.adapter.infrastructureLayer.event.projection.GameStateRepositoryInRedis
import eventDemo.business.event.projection.projectionListener.ReactionListener import eventDemo.business.event.projection.projectionListener.ReactionListener
import io.ktor.server.application.Application import org.koin.core.Koin
import org.koin.ktor.ext.get
fun Application.configureGameListener() { fun Koin.configureGameListener() {
ReactionListener(get(), get()) ReactionListener(get())
.init() .subscribeToBus(get())
get<GameStateRepositoryInRedis>()
.subscribeToBus(get(), get())
get<GameListRepositoryInRedis>()
.subscribeToBus(get(), get())
} }

View File

@@ -43,7 +43,7 @@ fun Module.configureDIInfrastructure(config: Configuration) {
singleOf(::GameProjectionBusInMemory) bind GameProjectionBus::class singleOf(::GameProjectionBusInMemory) bind GameProjectionBus::class
single { single {
GameStateRepositoryInRedis(get(), get(), get(), get(), snapshotConfig = SnapshotConfig()) GameStateRepositoryInRedis(get(), get(), snapshotConfig = SnapshotConfig())
} bind GameStateRepository::class } bind GameStateRepository::class
single { single {

View File

@@ -2,6 +2,7 @@ package eventDemo
import eventDemo.business.entity.Card import eventDemo.business.entity.Card
import eventDemo.business.entity.Deck import eventDemo.business.entity.Deck
import eventDemo.configuration.business.configureGameListener
import eventDemo.configuration.injection.appKoinModule import eventDemo.configuration.injection.appKoinModule
import eventDemo.configuration.ktor.configuration import eventDemo.configuration.ktor.configuration
import io.ktor.server.config.ApplicationConfig import io.ktor.server.config.ApplicationConfig
@@ -11,6 +12,8 @@ import io.ktor.utils.io.KtorDsl
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.module.KoinApplicationDslMarker import org.koin.core.module.KoinApplicationDslMarker
import org.koin.dsl.koinApplication import org.koin.dsl.koinApplication
import redis.clients.jedis.UnifiedJedis
import javax.sql.DataSource
fun Deck.allCardCount(): Int = fun Deck.allCardCount(): Int =
stack.size + discard.size + playersHands.values.flatten().size stack.size + discard.size + playersHands.values.flatten().size
@@ -31,6 +34,25 @@ suspend fun testApplicationWithConfig(block: suspend ApplicationTestBuilder.(koi
} }
val koin = koinApplication { modules(appKoinModule(conf.configuration())) }.koin val koin = koinApplication { modules(appKoinModule(conf.configuration())) }.koin
koin.cleanDataTest()
koin.configureGameListener()
block(koin) block(koin)
} }
} }
fun DataSource.cleanEventSource() {
this.connection.prepareStatement(
"""
truncate event_stream;
""".trimIndent(),
)
}
fun UnifiedJedis.cleanProjections() {
flushAll()
}
fun Koin.cleanDataTest() {
get<DataSource>().cleanEventSource()
get<UnifiedJedis>().cleanProjections()
}

View File

@@ -13,7 +13,6 @@ import eventDemo.business.event.event.disableShuffleDeck
import eventDemo.business.event.projection.gameState.GameState import eventDemo.business.event.projection.gameState.GameState
import eventDemo.business.event.projection.gameState.apply import eventDemo.business.event.projection.gameState.apply
import eventDemo.business.event.projection.projectionListener.PlayerNotificationListener import eventDemo.business.event.projection.projectionListener.PlayerNotificationListener
import eventDemo.business.event.projection.projectionListener.ReactionListener
import eventDemo.business.notification.CommandSuccessNotification import eventDemo.business.notification.CommandSuccessNotification
import eventDemo.business.notification.ItsTheTurnOfNotification import eventDemo.business.notification.ItsTheTurnOfNotification
import eventDemo.business.notification.Notification import eventDemo.business.notification.Notification
@@ -22,6 +21,7 @@ import eventDemo.business.notification.PlayerAsPlayACardNotification
import eventDemo.business.notification.PlayerWasReadyNotification import eventDemo.business.notification.PlayerWasReadyNotification
import eventDemo.business.notification.TheGameWasStartedNotification import eventDemo.business.notification.TheGameWasStartedNotification
import eventDemo.business.notification.WelcomeToTheGameNotification import eventDemo.business.notification.WelcomeToTheGameNotification
import eventDemo.configuration.business.configureGameListener
import eventDemo.libs.event.projection.ProjectionSnapshotRepositoryInMemory import eventDemo.libs.event.projection.ProjectionSnapshotRepositoryInMemory
import eventDemo.testKoinApplicationWithConfig import eventDemo.testKoinApplicationWithConfig
import io.kotest.assertions.nondeterministic.until import io.kotest.assertions.nondeterministic.until
@@ -177,7 +177,7 @@ class GameSimulationTest :
val commandHandler by inject<GameCommandHandler>() val commandHandler by inject<GameCommandHandler>()
val eventStore by inject<GameEventStore>() val eventStore by inject<GameEventStore>()
val playerNotificationListener by inject<PlayerNotificationListener>() val playerNotificationListener by inject<PlayerNotificationListener>()
ReactionListener(get(), get()).init() configureGameListener()
playerNotificationListener.startListening(player1, gameId) { channelNotification1.trySendBlocking(it) } playerNotificationListener.startListening(player1, gameId) { channelNotification1.trySendBlocking(it) }
playerNotificationListener.startListening(player2, gameId) { channelNotification2.trySendBlocking(it) } playerNotificationListener.startListening(player2, gameId) { channelNotification2.trySendBlocking(it) }

View File

@@ -5,10 +5,10 @@ import eventDemo.business.command.command.IWantToJoinTheGameCommand
import eventDemo.business.entity.GameId import eventDemo.business.entity.GameId
import eventDemo.business.entity.Player import eventDemo.business.entity.Player
import eventDemo.business.event.projection.projectionListener.PlayerNotificationListener import eventDemo.business.event.projection.projectionListener.PlayerNotificationListener
import eventDemo.business.event.projection.projectionListener.ReactionListener
import eventDemo.business.notification.CommandSuccessNotification import eventDemo.business.notification.CommandSuccessNotification
import eventDemo.business.notification.Notification import eventDemo.business.notification.Notification
import eventDemo.business.notification.WelcomeToTheGameNotification import eventDemo.business.notification.WelcomeToTheGameNotification
import eventDemo.configuration.business.configureGameListener
import eventDemo.testKoinApplicationWithConfig import eventDemo.testKoinApplicationWithConfig
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldContain
@@ -28,13 +28,13 @@ class GameCommandHandlerTest :
test("handle a command should execute the command") { test("handle a command should execute the command") {
withTimeout(1.seconds) { withTimeout(1.seconds) {
testKoinApplicationWithConfig { testKoinApplicationWithConfig {
configureGameListener()
val commandHandler by inject<GameCommandHandler>() val commandHandler by inject<GameCommandHandler>()
val notificationListener by inject<PlayerNotificationListener>() val notificationListener by inject<PlayerNotificationListener>()
val gameId = GameId() val gameId = GameId()
val player = Player("Tesla") val player = Player("Tesla")
val channelCommand = Channel<GameCommand>(Channel.BUFFERED) val channelCommand = Channel<GameCommand>(Channel.BUFFERED)
val channelNotification = Channel<Notification>(Channel.BUFFERED) val channelNotification = Channel<Notification>(Channel.BUFFERED)
ReactionListener(get(), get()).init()
notificationListener.startListening( notificationListener.startListening(
player, player,
gameId, gameId,

View File

@@ -6,7 +6,7 @@ import eventDemo.business.event.GameEventHandler
import eventDemo.business.event.event.NewPlayerEvent import eventDemo.business.event.event.NewPlayerEvent
import eventDemo.business.event.projection.gameState.GameState import eventDemo.business.event.projection.gameState.GameState
import eventDemo.business.event.projection.gameState.GameStateRepository import eventDemo.business.event.projection.gameState.GameStateRepository
import eventDemo.testKoinApplicationWithConfig import eventDemo.testApplicationWithConfig
import io.kotest.assertions.nondeterministic.eventually import io.kotest.assertions.nondeterministic.eventually
import io.kotest.assertions.nondeterministic.eventuallyConfig import io.kotest.assertions.nondeterministic.eventuallyConfig
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
@@ -18,7 +18,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.context.stopKoin
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -30,9 +29,9 @@ class GameStateRepositoryTest :
test("GameStateRepository should build the projection when a new event occurs") { test("GameStateRepository should build the projection when a new event occurs") {
val aggregateId = GameId() val aggregateId = GameId()
testKoinApplicationWithConfig { testApplicationWithConfig { koin ->
val repo = get<GameStateRepository>() val repo = koin.get<GameStateRepository>()
val eventHandler = get<GameEventHandler>() val eventHandler = koin.get<GameEventHandler>()
eventHandler eventHandler
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) } .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
.also { event -> .also { event ->
@@ -47,15 +46,14 @@ class GameStateRepositoryTest :
} }
} }
} }
stopKoin()
} }
test("get should build the last version of the state") { test("get should build the last version of the state") {
val aggregateId = GameId() val aggregateId = GameId()
testKoinApplicationWithConfig { testApplicationWithConfig { koin ->
val repo = get<GameStateRepository>() val repo = koin.get<GameStateRepository>()
val eventHandler = get<GameEventHandler>() val eventHandler = koin.get<GameEventHandler>()
val projectionBus = get<GameProjectionBus>() val projectionBus = koin.get<GameProjectionBus>()
var state: GameState? = null var state: GameState? = null
projectionBus.subscribe { projectionBus.subscribe {
@@ -88,9 +86,9 @@ class GameStateRepositoryTest :
test("getUntil should build the state until the event") { test("getUntil should build the state until the event") {
repeat(10) { repeat(10) {
val aggregateId = GameId() val aggregateId = GameId()
testKoinApplicationWithConfig { testApplicationWithConfig { koin ->
val repo = get<GameStateRepository>() val repo = koin.get<GameStateRepository>()
val eventHandler = get<GameEventHandler>() val eventHandler = koin.get<GameEventHandler>()
val event1 = val event1 =
eventHandler eventHandler
@@ -117,9 +115,9 @@ class GameStateRepositoryTest :
test("getUntil should be concurrently secure") { test("getUntil should be concurrently secure") {
val aggregateId = GameId() val aggregateId = GameId()
testKoinApplicationWithConfig { testApplicationWithConfig { koin ->
val repo = get<GameStateRepository>() val repo = koin.get<GameStateRepository>()
val eventHandler = get<GameEventHandler>() val eventHandler = koin.get<GameEventHandler>()
(1..10) (1..10)
.map { r -> .map { r ->

View File

@@ -1,5 +1,6 @@
package eventDemo.business.event.projection package eventDemo.business.event.projection
import eventDemo.cleanProjections
import eventDemo.configuration.serializer.UUIDSerializer import eventDemo.configuration.serializer.UUIDSerializer
import eventDemo.libs.event.AggregateId import eventDemo.libs.event.AggregateId
import eventDemo.libs.event.Event import eventDemo.libs.event.Event
@@ -96,6 +97,37 @@ class ProjectionSnapshotRepositoryTest :
} }
} }
context("getList method must be return all inserted events") {
withData(list) { (eventStore, repo) ->
val aggregateId = IdTest()
val otherAggregateId = IdTest()
val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = otherAggregateId)
eventStore.publish(eventOther)
repo.applyAndPutToCache(eventOther)
assertNotNull(repo.getUntil(eventOther)).also {
assertNotNull(it.value) shouldBeEqual "valOther"
}
val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId)
eventStore.publish(event1)
repo.applyAndPutToCache(event1)
val event2 = Event2Test(value2 = "val2", version = 2, aggregateId = aggregateId)
eventStore.publish(event2)
repo.applyAndPutToCache(event2)
repo.getList().apply {
any { it.aggregateId == otherAggregateId } shouldBeEqual true
any { it.aggregateId == aggregateId } shouldBeEqual true
any { it.value == "val1val2" } shouldBeEqual true
any { it.value == "valOther" } shouldBeEqual true
any { it.lastEventVersion == 2 } shouldBeEqual true
any { it.lastEventVersion == 1 } shouldBeEqual true
}
}
}
context("ProjectionSnapshotRepository should be thread safe") { context("ProjectionSnapshotRepository should be thread safe") {
continually(1.seconds) { continually(1.seconds) {
withData(list) { (eventStore, repo) -> withData(list) { (eventStore, repo) ->
@@ -199,10 +231,12 @@ private fun getSnapshotRepoInMemoryTest(
private fun getSnapshotRepoInRedisTest( private fun getSnapshotRepoInRedisTest(
eventStore: EventStore<TestEvents, IdTest>, eventStore: EventStore<TestEvents, IdTest>,
snapshotConfig: SnapshotConfig, snapshotConfig: SnapshotConfig,
): ProjectionSnapshotRepository<TestEvents, ProjectionTest, IdTest> = ): ProjectionSnapshotRepository<TestEvents, ProjectionTest, IdTest> {
ProjectionSnapshotRepositoryInRedis( val jedis = JedisPooled("redis://localhost:6379")
jedis.cleanProjections()
return ProjectionSnapshotRepositoryInRedis(
eventStore = eventStore, eventStore = eventStore,
jedis = JedisPooled("redis://localhost:6379"), jedis = jedis,
initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) }, initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) },
snapshotCacheConfig = snapshotConfig, snapshotCacheConfig = snapshotConfig,
projectionClass = ProjectionTest::class, projectionClass = ProjectionTest::class,
@@ -210,16 +244,17 @@ private fun getSnapshotRepoInRedisTest(
jsonToProjection = { Json.decodeFromString(it) }, jsonToProjection = { Json.decodeFromString(it) },
applyToProjection = apply, applyToProjection = apply,
) )
}
private val apply: ProjectionTest.(TestEvents) -> ProjectionTest = { event -> private val apply: ProjectionTest.(TestEvents) -> ProjectionTest = { event ->
this.let { projection -> this.let { projection ->
when (event) { when (event) {
is Event1Test -> { is Event1Test -> {
projection.copy(value = (projection.value ?: "") + event.value1) projection.copy(value = (projection.value.orEmpty()) + event.value1)
} }
is Event2Test -> { is Event2Test -> {
projection.copy(value = (projection.value ?: "") + event.value2) projection.copy(value = (projection.value.orEmpty()) + event.value2)
} }
is EventXTest -> { is EventXTest -> {

View File

@@ -1,5 +1,6 @@
package eventDemo.libs.event package eventDemo.libs.event
import eventDemo.cleanEventSource
import eventDemo.testKoinApplicationWithConfig import eventDemo.testKoinApplicationWithConfig
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.datatest.withData import io.kotest.datatest.withData
@@ -12,6 +13,7 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.junit.jupiter.api.assertNull import org.junit.jupiter.api.assertNull
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import javax.sql.DataSource
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
@DelicateCoroutinesApi @DelicateCoroutinesApi
@@ -27,6 +29,7 @@ class EventStreamTest :
suspend fun eventStreams(): List<EventStream<EventXTest, IdTest>> = suspend fun eventStreams(): List<EventStream<EventXTest, IdTest>> =
testKoinApplicationWithConfig { testKoinApplicationWithConfig {
get<DataSource>().cleanEventSource()
listOf( listOf(
EventStreamInMemory(IdTest()), EventStreamInMemory(IdTest()),
EventStreamInPostgresql( EventStreamInPostgresql(