feat: remove snapshot on ProjectionRepository
This commit is contained in:
@@ -6,6 +6,7 @@ import eventDemo.business.entity.Deck
|
||||
import eventDemo.configuration.business.configureGameListener
|
||||
import eventDemo.configuration.injection.appKoinModule
|
||||
import eventDemo.configuration.ktor.configuration
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.ktor.server.config.ApplicationConfig
|
||||
import io.ktor.server.testing.ApplicationTestBuilder
|
||||
import io.ktor.server.testing.testApplication
|
||||
@@ -39,6 +40,7 @@ fun testApplicationWithConfig(
|
||||
configBuilder: Koin.() -> Unit = {},
|
||||
block: suspend ApplicationTestBuilder.() -> Unit,
|
||||
) {
|
||||
val logger = KotlinLogging.logger {}
|
||||
testApplication {
|
||||
val conf = ApplicationConfig("application.conf")
|
||||
environment {
|
||||
@@ -46,11 +48,18 @@ fun testApplicationWithConfig(
|
||||
}
|
||||
|
||||
application {
|
||||
logger.info { "Config App" }
|
||||
val koin = getKoin()
|
||||
koin.cleanDataTest()
|
||||
configBuilder(koin)
|
||||
runCatching {
|
||||
logger.info { "Starting A" }
|
||||
configBuilder(koin)
|
||||
logger.info { "A finish" }
|
||||
}
|
||||
}
|
||||
block()
|
||||
logger.info { "Starting B" }
|
||||
this@testApplication.block()
|
||||
logger.info { "B finish" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import eventDemo.business.event.event.NewPlayerEvent
|
||||
import eventDemo.business.event.event.PlayerReadyEvent
|
||||
import eventDemo.business.event.projection.GameList
|
||||
import eventDemo.testApplicationWithConfig
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.kotest.assertions.nondeterministic.eventually
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.collections.shouldContain
|
||||
@@ -19,18 +20,19 @@ import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
val logger = KotlinLogging.logger {}
|
||||
|
||||
class GameListRouteTest :
|
||||
FunSpec({
|
||||
test("/games with no game started") {
|
||||
|
||||
testApplicationWithConfig {
|
||||
val player1 = Player(name = "Nikola")
|
||||
|
||||
logger.info { "Starting player1" }
|
||||
httpClient()
|
||||
.get("/games") {
|
||||
withAuth(player1)
|
||||
@@ -48,16 +50,14 @@ class GameListRouteTest :
|
||||
val player1 = Player(name = "Nikola")
|
||||
testApplicationWithConfig(
|
||||
{
|
||||
runBlocking {
|
||||
get<GameEventHandler>()
|
||||
.handle(gameId) {
|
||||
NewPlayerEvent(gameId, player1, it)
|
||||
}
|
||||
}
|
||||
get<GameEventHandler>()
|
||||
.handle(gameId) {
|
||||
NewPlayerEvent(gameId, player1, it)
|
||||
}
|
||||
},
|
||||
) {
|
||||
// Wait until the projection is created
|
||||
eventually(1.seconds) {
|
||||
eventually(3.seconds) {
|
||||
httpClient()
|
||||
.get("/games") {
|
||||
withAuth(player1)
|
||||
@@ -81,19 +81,17 @@ class GameListRouteTest :
|
||||
val player2 = Player(name = "Einstein")
|
||||
testApplicationWithConfig({
|
||||
val eventHandler = get<GameEventHandler>()
|
||||
runBlocking {
|
||||
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
|
||||
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) }
|
||||
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) }
|
||||
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) }
|
||||
eventHandler.handle(gameId) {
|
||||
GameStartedEvent.new(
|
||||
gameId,
|
||||
setOf(player1, player2),
|
||||
it,
|
||||
shuffleIsDisabled = true,
|
||||
)
|
||||
}
|
||||
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
|
||||
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) }
|
||||
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) }
|
||||
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) }
|
||||
eventHandler.handle(gameId) {
|
||||
GameStartedEvent.new(
|
||||
gameId,
|
||||
setOf(player1, player2),
|
||||
it,
|
||||
shuffleIsDisabled = true,
|
||||
)
|
||||
}
|
||||
}) {
|
||||
eventually(1.seconds) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package eventDemo.adapter.interfaceLayer.query
|
||||
|
||||
import eventDemo.Tag
|
||||
import eventDemo.adapter.infrastructureLayer.event.projection.GameStateRepositoryInMemory
|
||||
import eventDemo.business.command.GameCommandHandler
|
||||
import eventDemo.business.command.command.GameCommand
|
||||
import eventDemo.business.command.command.IWantToJoinTheGameCommand
|
||||
@@ -10,9 +9,9 @@ import eventDemo.business.command.command.IamReadyToPlayCommand
|
||||
import eventDemo.business.entity.Card
|
||||
import eventDemo.business.entity.GameId
|
||||
import eventDemo.business.entity.Player
|
||||
import eventDemo.business.event.GameEventStore
|
||||
import eventDemo.business.event.event.disableShuffleDeck
|
||||
import eventDemo.business.event.projection.GameState
|
||||
import eventDemo.business.event.projection.GameStateRepository
|
||||
import eventDemo.business.event.projection.projectionListener.PlayerNotificationListener
|
||||
import eventDemo.business.notification.CommandSuccessNotification
|
||||
import eventDemo.business.notification.ItsTheTurnOfNotification
|
||||
@@ -61,9 +60,9 @@ class GameSimulationTest :
|
||||
var player1HasJoin = false
|
||||
|
||||
testKoinApplicationWithConfig {
|
||||
val commandHandler by inject<GameCommandHandler>()
|
||||
val eventStore by inject<GameEventStore>()
|
||||
val playerNotificationListener by inject<PlayerNotificationListener>()
|
||||
val commandHandler = get<GameCommandHandler>()
|
||||
val playerNotificationListener = get<PlayerNotificationListener>()
|
||||
val gameStateRepository = get<GameStateRepository>()
|
||||
|
||||
// Run command handler
|
||||
// In the normal process, these handlers is invoque players connect to the websocket
|
||||
@@ -76,8 +75,8 @@ class GameSimulationTest :
|
||||
}
|
||||
}
|
||||
|
||||
// Consume etch notification of players, and put theses in list.
|
||||
// is used later to control when other players can be executing the next action
|
||||
// Consume etch notification of players, and put theses in a list.
|
||||
// Is used later to control when other players can execute the next action
|
||||
val player1Notifications = mutableListOf<Notification>()
|
||||
val player2Notifications = mutableListOf<Notification>()
|
||||
run {
|
||||
@@ -94,7 +93,7 @@ class GameSimulationTest :
|
||||
}
|
||||
}
|
||||
|
||||
// The player 1 actions
|
||||
// Player 1 actions
|
||||
val player1Job =
|
||||
launch {
|
||||
playerNotificationListener.startListening(player1, gameId) {
|
||||
@@ -132,11 +131,11 @@ class GameSimulationTest :
|
||||
}
|
||||
}
|
||||
|
||||
// The player 2 actions
|
||||
// Player 2 actions
|
||||
val player2Job =
|
||||
launch {
|
||||
// wait the player 1 has join the game
|
||||
until(1.seconds) { player1HasJoin }
|
||||
// wait player 1 has joined the game
|
||||
until(3.seconds) { player1HasJoin }
|
||||
|
||||
playerNotificationListener.startListening(player2, gameId) {
|
||||
channelNotification2.trySendBlocking(it)
|
||||
@@ -176,7 +175,7 @@ class GameSimulationTest :
|
||||
joinAll(player1Job, player2Job)
|
||||
|
||||
// Build the last state from the event store
|
||||
val state = GameStateRepositoryInMemory(eventStore = eventStore).getLast(gameId)
|
||||
val state = gameStateRepository.get(gameId)
|
||||
|
||||
// Check if the state is correct
|
||||
state.aggregateId shouldBeEqual gameId
|
||||
@@ -192,6 +191,6 @@ class GameSimulationTest :
|
||||
})
|
||||
|
||||
private suspend inline fun <reified T : Notification> MutableList<Notification>.waitNotification(crossinline block: T.() -> Boolean): T =
|
||||
eventually(1.seconds) {
|
||||
eventually(3.seconds) {
|
||||
filterIsInstance<T>().first { block(it) }
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import eventDemo.business.event.projection.GameState
|
||||
import eventDemo.business.event.projection.GameStateRepository
|
||||
import eventDemo.testApplicationWithConfig
|
||||
import io.kotest.assertions.nondeterministic.eventually
|
||||
import io.kotest.assertions.nondeterministic.until
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.collections.shouldHaveSize
|
||||
import io.kotest.matchers.equals.shouldBeEqual
|
||||
@@ -53,7 +54,6 @@ class GameStateRouteTest :
|
||||
val gameId = GameId()
|
||||
val player1 = Player(name = "Nikola")
|
||||
val player2 = Player(name = "Einstein")
|
||||
var lastPlayedCard: Card? = null
|
||||
testApplicationWithConfig({
|
||||
disableShuffleDeck()
|
||||
val eventHandler = get<GameEventHandler>()
|
||||
@@ -64,9 +64,8 @@ class GameStateRouteTest :
|
||||
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) }
|
||||
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) }
|
||||
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) }
|
||||
lastPlayedCard = eventually { stateRepo.getLast(gameId).playableCards(player1).first() }
|
||||
assertNotNull(lastPlayedCard)
|
||||
.let { assertIs<Card.NumericCard>(lastPlayedCard) }
|
||||
val lastPlayedCard = eventually(3.seconds) { stateRepo.get(gameId).playableCards(player1).first() }
|
||||
assertIs<Card.NumericCard>(lastPlayedCard)
|
||||
.let {
|
||||
it.number shouldBeEqual 0
|
||||
it.color shouldBeEqual Card.Color.Red
|
||||
@@ -74,32 +73,36 @@ class GameStateRouteTest :
|
||||
eventHandler.handle(gameId) {
|
||||
CardIsPlayedEvent(
|
||||
gameId,
|
||||
assertNotNull(lastPlayedCard),
|
||||
lastPlayedCard,
|
||||
player1,
|
||||
it,
|
||||
)
|
||||
}
|
||||
until(3.seconds) {
|
||||
stateRepo
|
||||
.get(gameId)
|
||||
.deck.discard
|
||||
.last() == lastPlayedCard
|
||||
}
|
||||
}
|
||||
}) {
|
||||
eventually(1.seconds) {
|
||||
httpClient()
|
||||
.get("/games/$gameId/state") {
|
||||
withAuth(player1)
|
||||
accept(ContentType.Application.Json)
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
||||
call.body<GameState>().apply {
|
||||
aggregateId shouldBeEqual gameId
|
||||
players shouldHaveSize 2
|
||||
isStarted shouldBeEqual true
|
||||
assertIs<CardIsPlayedEvent>(lastEvent)
|
||||
readyPlayers shouldBeEqual setOf(player1, player2)
|
||||
direction shouldBeEqual GameState.Direction.CLOCKWISE
|
||||
assertNotNull(lastCardPlayer) shouldBeEqual player1
|
||||
assertNotNull(colorOnCurrentStack) shouldBeEqual Card.Color.Red
|
||||
}
|
||||
httpClient()
|
||||
.get("/games/$gameId/state") {
|
||||
withAuth(player1)
|
||||
accept(ContentType.Application.Json)
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
||||
call.body<GameState>().apply {
|
||||
aggregateId shouldBeEqual gameId
|
||||
players shouldHaveSize 2
|
||||
isStarted shouldBeEqual true
|
||||
assertIs<CardIsPlayedEvent>(lastEvent)
|
||||
readyPlayers shouldBeEqual setOf(player1, player2)
|
||||
direction shouldBeEqual GameState.Direction.CLOCKWISE
|
||||
assertNotNull(lastCardPlayer) shouldBeEqual player1
|
||||
assertNotNull(colorOnCurrentStack) shouldBeEqual Card.Color.Red
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,9 +121,8 @@ class GameStateRouteTest :
|
||||
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) }
|
||||
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) }
|
||||
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) }
|
||||
lastPlayedCard = eventually { stateRepo.getLast(gameId).playableCards(player1).first() }
|
||||
assertNotNull(lastPlayedCard)
|
||||
.let { assertIs<Card.NumericCard>(lastPlayedCard) }
|
||||
lastPlayedCard = eventually(3.seconds) { stateRepo.get(gameId).playableCards(player1).first() }
|
||||
assertIs<Card.NumericCard>(lastPlayedCard)
|
||||
.let {
|
||||
it.number shouldBeEqual 0
|
||||
it.color shouldBeEqual Card.Color.Red
|
||||
@@ -133,18 +135,23 @@ class GameStateRouteTest :
|
||||
it,
|
||||
)
|
||||
}
|
||||
|
||||
until(3.seconds) {
|
||||
stateRepo
|
||||
.get(gameId)
|
||||
.deck.discard
|
||||
.last() == lastPlayedCard
|
||||
}
|
||||
}
|
||||
}) {
|
||||
eventually(1.seconds) {
|
||||
httpClient()
|
||||
.get("/games/$gameId/card/last") {
|
||||
withAuth(player1)
|
||||
accept(ContentType.Application.Json)
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
||||
assertEquals(assertNotNull(lastPlayedCard), call.body<Card>())
|
||||
}
|
||||
}
|
||||
httpClient()
|
||||
.get("/games/$gameId/card/last") {
|
||||
withAuth(player1)
|
||||
accept(ContentType.Application.Json)
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
||||
assertEquals(assertNotNull(lastPlayedCard), call.body<Card>())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ package eventDemo.business.event.projection
|
||||
|
||||
import ch.qos.logback.classic.Level
|
||||
import com.rabbitmq.client.impl.ForgivingExceptionHandler
|
||||
import com.zaxxer.hikari.pool.ProxyConnection
|
||||
import eventDemo.Tag
|
||||
import eventDemo.business.command.GameCommandHandler
|
||||
import eventDemo.business.entity.GameId
|
||||
@@ -17,7 +16,6 @@ import io.kotest.common.KotestInternal
|
||||
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
|
||||
@@ -41,13 +39,10 @@ class GameStateRepositoryTest :
|
||||
val eventHandler = get<GameEventHandler>()
|
||||
eventHandler
|
||||
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
|
||||
.also { event ->
|
||||
.also {
|
||||
// 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(repo.get(aggregateId)).also {
|
||||
assertNotNull(it.players) shouldBeEqual setOf(player1)
|
||||
}
|
||||
}
|
||||
@@ -68,9 +63,9 @@ class GameStateRepositoryTest :
|
||||
|
||||
var state: GameState? = null
|
||||
projectionBus.subscribe {
|
||||
repo.getLast(aggregateId).also {
|
||||
state = it
|
||||
}
|
||||
repo
|
||||
.get(aggregateId)
|
||||
.also { state = it }
|
||||
}
|
||||
|
||||
eventHandler
|
||||
@@ -86,7 +81,7 @@ class GameStateRepositoryTest :
|
||||
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player2, version = it) }
|
||||
.also {
|
||||
eventually(1.seconds) {
|
||||
assertNotNull(repo.getLast(aggregateId)).also {
|
||||
assertNotNull(repo.get(aggregateId)).also {
|
||||
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
|
||||
}
|
||||
}
|
||||
@@ -95,44 +90,7 @@ class GameStateRepositoryTest :
|
||||
}
|
||||
}
|
||||
|
||||
test("getUntil should build the state until the event") {
|
||||
withLogLevel(
|
||||
GameCommandHandler::class.java.name to Level.ERROR,
|
||||
ForgivingExceptionHandler::class.java.name to Level.OFF,
|
||||
ProxyConnection::class.java.name to Level.OFF,
|
||||
Logger.ROOT_LOGGER_NAME to Level.INFO,
|
||||
) {
|
||||
repeat(10) {
|
||||
val aggregateId = GameId()
|
||||
testKoinApplicationWithConfig {
|
||||
val repo = get<GameStateRepository>()
|
||||
val eventHandler = get<GameEventHandler>()
|
||||
|
||||
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").config(tags = setOf(Tag.Concurrence)) {
|
||||
test("get should be concurrently secure").config(tags = setOf(Tag.Concurrence)) {
|
||||
withLogLevel(
|
||||
Logger.ROOT_LOGGER_NAME to Level.ERROR,
|
||||
ForgivingExceptionHandler::class.java.name to Level.OFF,
|
||||
@@ -167,17 +125,12 @@ class GameStateRepositoryTest :
|
||||
includeFirst = false
|
||||
},
|
||||
) {
|
||||
repo.getLast(aggregateId).run {
|
||||
repo.get(aggregateId).run {
|
||||
lastEventVersion shouldBeEqual 200
|
||||
players shouldHaveSize 200
|
||||
}
|
||||
repo.count(aggregateId) shouldBe 21
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xtest("get should be concurrently secure") {
|
||||
tags(Tag.Concurrence)
|
||||
}
|
||||
})
|
||||
|
||||
38
src/test/kotlin/eventDemo/libs/event/EventHandlerTest.kt
Normal file
38
src/test/kotlin/eventDemo/libs/event/EventHandlerTest.kt
Normal file
@@ -0,0 +1,38 @@
|
||||
package eventDemo.libs.event
|
||||
|
||||
import eventDemo.libs.bus.Bus
|
||||
import eventDemo.libs.bus.BusInMemory
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.equals.shouldBeEqual
|
||||
|
||||
class EventHandlerTest :
|
||||
FunSpec({
|
||||
test("EventHandler::handle should returns the built event") {
|
||||
val eventBus: Bus<EventXTest> = BusInMemory()
|
||||
val eventStore: EventStore<EventXTest, IdTest> = EventStoreInMemory()
|
||||
val versionBuilder: VersionBuilder = VersionBuilderLocal()
|
||||
val aggregateId: IdTest = IdTest()
|
||||
val handler =
|
||||
EventHandlerImpl(
|
||||
eventBus,
|
||||
eventStore,
|
||||
versionBuilder,
|
||||
)
|
||||
|
||||
// When
|
||||
val event =
|
||||
handler.handle(aggregateId) {
|
||||
EventXTest(aggregateId = aggregateId, version = it, num = 1)
|
||||
}
|
||||
|
||||
// Then
|
||||
event.aggregateId shouldBeEqual aggregateId
|
||||
event.version shouldBeEqual 1
|
||||
}
|
||||
|
||||
xtest("EventHandler::handle should publish the event into the store")
|
||||
|
||||
xtest("EventHandler::handle should publish the event into the bus")
|
||||
|
||||
xtest("EventHandler::handle should publish the event into the bus in incremental order")
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
package eventDemo.business.event.projection
|
||||
package eventDemo.libs.event.projection
|
||||
|
||||
import eventDemo.cleanProjections
|
||||
import eventDemo.configuration.serializer.UUIDSerializer
|
||||
@@ -7,12 +7,6 @@ import eventDemo.libs.event.Event
|
||||
import eventDemo.libs.event.EventStore
|
||||
import eventDemo.libs.event.EventStoreInMemory
|
||||
import eventDemo.libs.event.VersionBuilderLocal
|
||||
import eventDemo.libs.event.projection.DISABLED_CONFIG
|
||||
import eventDemo.libs.event.projection.Projection
|
||||
import eventDemo.libs.event.projection.ProjectionSnapshotRepository
|
||||
import eventDemo.libs.event.projection.ProjectionSnapshotRepositoryInMemory
|
||||
import eventDemo.libs.event.projection.ProjectionSnapshotRepositoryInRedis
|
||||
import eventDemo.libs.event.projection.SnapshotConfig
|
||||
import io.kotest.assertions.nondeterministic.continually
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.datatest.withData
|
||||
@@ -22,6 +16,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -34,14 +29,14 @@ import kotlin.test.assertNotNull
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
class ProjectionSnapshotRepositoryTest :
|
||||
class ProjectionRepositoryTest :
|
||||
FunSpec({
|
||||
data class TestData(
|
||||
val store: EventStore<TestEvents, IdTest>,
|
||||
val snapshotRepo: ProjectionSnapshotRepository<TestEvents, ProjectionTest, IdTest>,
|
||||
val repository: ProjectionRepository<TestEvents, ProjectionTest, IdTest>,
|
||||
) : WithDataTestName {
|
||||
override fun dataTestName(): String =
|
||||
"${snapshotRepo::class.simpleName} with ${store::class.simpleName}"
|
||||
"${repository::class.simpleName} with ${store::class.simpleName}"
|
||||
}
|
||||
|
||||
val eventStores =
|
||||
@@ -50,48 +45,40 @@ class ProjectionSnapshotRepositoryTest :
|
||||
)
|
||||
val projectionRepo =
|
||||
listOf(
|
||||
::getSnapshotRepoInMemoryTest,
|
||||
::getSnapshotRepoInRedisTest,
|
||||
::getRepoInMemoryTest,
|
||||
::getRepoInRedisTest,
|
||||
)
|
||||
|
||||
val list =
|
||||
eventStores.flatMap { store ->
|
||||
projectionRepo.map { repo ->
|
||||
store().let { store -> TestData(store, repo(store, DISABLED_CONFIG)) }
|
||||
TestData(store(), repo())
|
||||
}
|
||||
}
|
||||
|
||||
context("when call applyAndPutToCache, the getUntil method must be use the built projection cache") {
|
||||
context("when call applyAndSave, the projection should be built and save to the repository") {
|
||||
withData(list) { (eventStore, repo) ->
|
||||
val aggregateId = IdTest()
|
||||
|
||||
val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = IdTest())
|
||||
eventStore.publish(eventOther)
|
||||
repo.applyAndPutToCache(eventOther)
|
||||
assertNotNull(repo.getUntil(eventOther)).also {
|
||||
// eventStore.publish(eventOther)
|
||||
val p = repo.applyAndSave(eventOther)
|
||||
println(p)
|
||||
assertNotNull(repo.get(eventOther.aggregateId)).also {
|
||||
assertNotNull(it.value) shouldBeEqual "valOther"
|
||||
}
|
||||
|
||||
val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId)
|
||||
eventStore.publish(event1)
|
||||
repo.applyAndPutToCache(event1)
|
||||
assertNotNull(repo.getLast(event1.aggregateId)).also {
|
||||
assertNotNull(it.value) shouldBeEqual "val1"
|
||||
}
|
||||
assertNotNull(repo.getUntil(event1)).also {
|
||||
// eventStore.publish(event1)
|
||||
repo.applyAndSave(event1)
|
||||
assertNotNull(repo.get(event1.aggregateId)).also {
|
||||
assertNotNull(it.value) shouldBeEqual "val1"
|
||||
}
|
||||
|
||||
val event2 = Event2Test(value2 = "val2", version = 2, aggregateId = aggregateId)
|
||||
eventStore.publish(event2)
|
||||
repo.applyAndPutToCache(event2)
|
||||
assertNotNull(repo.getLast(event2.aggregateId)).also {
|
||||
assertNotNull(it.value) shouldBeEqual "val1val2"
|
||||
}
|
||||
assertNotNull(repo.getUntil(event1)).also {
|
||||
assertNotNull(it.value) shouldBeEqual "val1"
|
||||
}
|
||||
assertNotNull(repo.getUntil(event2)).also {
|
||||
// eventStore.publish(event2)
|
||||
repo.applyAndSave(event2)
|
||||
assertNotNull(repo.get(event2.aggregateId)).also {
|
||||
assertNotNull(it.value) shouldBeEqual "val1val2"
|
||||
}
|
||||
}
|
||||
@@ -104,18 +91,18 @@ class ProjectionSnapshotRepositoryTest :
|
||||
|
||||
val eventOther = Event2Test(value2 = "valOther", version = 1, aggregateId = otherAggregateId)
|
||||
eventStore.publish(eventOther)
|
||||
repo.applyAndPutToCache(eventOther)
|
||||
assertNotNull(repo.getUntil(eventOther)).also {
|
||||
repo.applyAndSave(eventOther)
|
||||
assertNotNull(repo.get(eventOther.aggregateId)).also {
|
||||
assertNotNull(it.value) shouldBeEqual "valOther"
|
||||
}
|
||||
|
||||
val event1 = Event1Test(value1 = "val1", version = 1, aggregateId = aggregateId)
|
||||
eventStore.publish(event1)
|
||||
repo.applyAndPutToCache(event1)
|
||||
repo.applyAndSave(event1)
|
||||
|
||||
val event2 = Event2Test(value2 = "val2", version = 2, aggregateId = aggregateId)
|
||||
eventStore.publish(event2)
|
||||
repo.applyAndPutToCache(event2)
|
||||
repo.applyAndSave(event2)
|
||||
|
||||
repo.getList().apply {
|
||||
any { it.aggregateId == otherAggregateId } shouldBeEqual true
|
||||
@@ -128,7 +115,7 @@ class ProjectionSnapshotRepositoryTest :
|
||||
}
|
||||
}
|
||||
|
||||
context("ProjectionSnapshotRepository should be thread safe") {
|
||||
context("ProjectionRepository should be thread safe") {
|
||||
continually(1.seconds) {
|
||||
withData(list) { (eventStore, repo) ->
|
||||
val aggregateId = IdTest()
|
||||
@@ -137,43 +124,24 @@ class ProjectionSnapshotRepositoryTest :
|
||||
(0..9)
|
||||
.map {
|
||||
GlobalScope.launch {
|
||||
(1..10).forEach {
|
||||
val eventX =
|
||||
lock.withLock {
|
||||
EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId)
|
||||
.also { eventStore.publish(it) }
|
||||
repeat(10) {
|
||||
lock.withLock {
|
||||
runBlocking {
|
||||
EventXTest(
|
||||
num = 1,
|
||||
version = versionBuilder.buildNextVersion(aggregateId),
|
||||
aggregateId = aggregateId,
|
||||
).also { repo.applyAndSave(it) }
|
||||
}
|
||||
repo.applyAndPutToCache(eventX)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.joinAll()
|
||||
assertNotNull(repo.getLast(aggregateId)).num shouldBeEqual 100
|
||||
assertNotNull(repo.count(aggregateId)) shouldBeEqual 100
|
||||
assertNotNull(repo.get(aggregateId)).lastEventVersion shouldBeEqual 100
|
||||
assertNotNull(repo.get(aggregateId)).num shouldBeEqual 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("removeOldSnapshot") {
|
||||
withData(list) { (eventStore, repo) ->
|
||||
val versionBuilder = VersionBuilderLocal()
|
||||
val aggregateId = IdTest()
|
||||
|
||||
suspend fun buildEndSendEventX() {
|
||||
EventXTest(num = 1, version = versionBuilder.buildNextVersion(aggregateId), aggregateId = aggregateId)
|
||||
.also { eventStore.publish(it) }
|
||||
.also { repo.applyAndPutToCache(it) }
|
||||
}
|
||||
|
||||
buildEndSendEventX()
|
||||
repo.getLast(aggregateId).num shouldBeEqual 1
|
||||
buildEndSendEventX()
|
||||
repo.getLast(aggregateId).num shouldBeEqual 2
|
||||
buildEndSendEventX()
|
||||
repo.getLast(aggregateId).num shouldBeEqual 3
|
||||
buildEndSendEventX()
|
||||
repo.getLast(aggregateId).num shouldBeEqual 4
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@JvmInline
|
||||
@@ -217,28 +185,18 @@ private data class EventXTest(
|
||||
val num: Int,
|
||||
) : TestEvents
|
||||
|
||||
private fun getSnapshotRepoInMemoryTest(
|
||||
eventStore: EventStore<TestEvents, IdTest>,
|
||||
snapshotConfig: SnapshotConfig,
|
||||
): ProjectionSnapshotRepository<TestEvents, ProjectionTest, IdTest> =
|
||||
ProjectionSnapshotRepositoryInMemory(
|
||||
eventStore = eventStore,
|
||||
private fun getRepoInMemoryTest(): ProjectionRepository<TestEvents, ProjectionTest, IdTest> =
|
||||
ProjectionRepositoryInMemory(
|
||||
initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) },
|
||||
snapshotCacheConfig = snapshotConfig,
|
||||
applyToProjection = apply,
|
||||
)
|
||||
|
||||
private fun getSnapshotRepoInRedisTest(
|
||||
eventStore: EventStore<TestEvents, IdTest>,
|
||||
snapshotConfig: SnapshotConfig,
|
||||
): ProjectionSnapshotRepository<TestEvents, ProjectionTest, IdTest> {
|
||||
private fun getRepoInRedisTest(): ProjectionRepository<TestEvents, ProjectionTest, IdTest> {
|
||||
val jedis = JedisPooled("redis://localhost:6379")
|
||||
jedis.cleanProjections()
|
||||
return ProjectionSnapshotRepositoryInRedis(
|
||||
eventStore = eventStore,
|
||||
return ProjectionRepositoryInRedis(
|
||||
jedis = jedis,
|
||||
initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) },
|
||||
snapshotCacheConfig = snapshotConfig,
|
||||
projectionClass = ProjectionTest::class,
|
||||
projectionToJson = { Json.encodeToString(it) },
|
||||
jsonToProjection = { Json.decodeFromString(it) },
|
||||
Reference in New Issue
Block a user