package eventDemo.business.event.projection import eventDemo.business.entity.GameId import eventDemo.business.entity.Player import eventDemo.business.event.GameEventHandler import eventDemo.business.event.event.NewPlayerEvent import eventDemo.business.event.projection.gameState.GameState import eventDemo.business.event.projection.gameState.GameStateRepository import eventDemo.testKoinApplicationWithConfig import io.kotest.assertions.nondeterministic.eventually import io.kotest.assertions.nondeterministic.eventuallyConfig import io.kotest.core.NamedTag import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.equals.shouldBeEqual import io.kotest.matchers.shouldBe import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlin.test.assertNotNull import kotlin.time.Duration.Companion.seconds @OptIn(DelicateCoroutinesApi::class) class GameStateRepositoryTest : FunSpec({ tags(NamedTag("postgresql")) val player1 = Player("Tesla") val player2 = Player(name = "Einstein") test("GameStateRepository should build the projection when a new event occurs") { val aggregateId = GameId() testKoinApplicationWithConfig { val repo = get() val eventHandler = get() eventHandler .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) } .also { event -> // Wait until the projection is created eventually(1.seconds) { assertNotNull(repo.getUntil(event)).also { assertNotNull(it.players) shouldBeEqual setOf(player1) } assertNotNull(repo.getLast(aggregateId)).also { assertNotNull(it.players) shouldBeEqual setOf(player1) } } } } } test("get should build the last version of the state") { val aggregateId = GameId() testKoinApplicationWithConfig { val repo = get() val eventHandler = get() val projectionBus = get() var state: GameState? = null projectionBus.subscribe { repo.getLast(aggregateId).also { state = it } } eventHandler .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) } .also { eventually(1.seconds) { assertNotNull(state).players.isNotEmpty() shouldBeEqual true assertNotNull(state).players shouldBeEqual setOf(player1) } } eventHandler .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) } .also { eventually(1.seconds) { assertNotNull(repo.getLast(aggregateId)).also { assertNotNull(it.players) shouldBeEqual setOf(player1, player2) } } } } } test("getUntil should build the state until the event") { repeat(10) { val aggregateId = GameId() testKoinApplicationWithConfig { val repo = get() val eventHandler = get() val event1 = eventHandler .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) } .also { event1 -> assertNotNull(repo.getUntil(event1)).also { assertNotNull(it.players) shouldBeEqual setOf(player1) } } eventHandler .handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) } .also { event2 -> assertNotNull(repo.getUntil(event2)).also { assertNotNull(it.players) shouldBeEqual setOf(player1, player2) } assertNotNull(repo.getUntil(event1)).also { assertNotNull(it.players) shouldBeEqual setOf(player1) } } } } } test("getUntil should be concurrently secure") { val aggregateId = GameId() testKoinApplicationWithConfig { val repo = get() val eventHandler = get() (1..10) .map { r -> GlobalScope .launch { repeat(100) { r2 -> val playerX = Player("player$r$r2") eventHandler .handle(aggregateId) { NewPlayerEvent( aggregateId = aggregateId, player = playerX, version = it, ) } } } }.joinAll() eventually( eventuallyConfig { duration = 10.seconds interval = 1.seconds includeFirst = false }, ) { repo.getLast(aggregateId).run { lastEventVersion shouldBeEqual 1000 players shouldHaveSize 1000 } repo.count(aggregateId) shouldBe 119 } } } xtest("get should be concurrently secure") { } })