diff --git a/src/main/kotlin/eventDemo/app/command/GameCommandHandler.kt b/src/main/kotlin/eventDemo/app/command/GameCommandHandler.kt index dd9fecc..a617e7c 100644 --- a/src/main/kotlin/eventDemo/app/command/GameCommandHandler.kt +++ b/src/main/kotlin/eventDemo/app/command/GameCommandHandler.kt @@ -11,6 +11,7 @@ import eventDemo.app.event.event.GameEvent import eventDemo.app.event.projection.GameStateRepository import eventDemo.app.notification.ErrorNotification import eventDemo.app.notification.Notification +import eventDemo.libs.command.CommandStreamChannelBuilder import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.SendChannel @@ -23,6 +24,7 @@ import kotlinx.coroutines.channels.SendChannel class GameCommandHandler( private val eventHandler: GameEventHandler, private val gameStateRepository: GameStateRepository, + private val commandStreamChannel: CommandStreamChannelBuilder, ) { private val logger = KotlinLogging.logger { } @@ -33,27 +35,43 @@ class GameCommandHandler( player: Player, incomingCommandChannel: ReceiveChannel, outgoingErrorChannelNotification: SendChannel, - ) = GameCommandStream(incomingCommandChannel).process { command -> - if (command.payload.player.id != player.id) { - nack() - } - - val playerErrorNotifier: suspend (String) -> Unit = { - val notification = ErrorNotification(message = it) - logger.atWarn { - message = "Notification send ERROR: ${notification.message}" - payload = mapOf("notification" to notification) + ) = commandStreamChannel(incomingCommandChannel) + .process { command -> + if (command.payload.player.id != player.id) { + logger.atWarn { + message = "Handle command Refuse, the player of the command is not the same: $command" + payload = mapOf("command" to command) + } + nack() + } else { + logger.atInfo { + message = "Handle command: $command" + payload = mapOf("command" to command) + } + command.run(outgoingErrorChannelNotification) } - outgoingErrorChannelNotification.send(notification) } - val gameState = gameStateRepository.get(command.payload.gameId) + private suspend fun GameCommand.run(outgoingErrorChannelNotification: SendChannel) { + val gameState = gameStateRepository.get(payload.gameId) + val playerErrorNotifier = errorNotifier(outgoingErrorChannelNotification) - when (command) { - is IWantToPlayCardCommand -> command.run(gameState, playerErrorNotifier, eventHandler) - is IamReadyToPlayCommand -> command.run(gameState, playerErrorNotifier, eventHandler) - is IWantToJoinTheGameCommand -> command.run(gameState, playerErrorNotifier, eventHandler) - is ICantPlayCommand -> command.run(gameState, playerErrorNotifier, eventHandler) + when (this) { + is IWantToPlayCardCommand -> run(gameState, playerErrorNotifier, eventHandler) + is IamReadyToPlayCommand -> run(gameState, playerErrorNotifier, eventHandler) + is IWantToJoinTheGameCommand -> run(gameState, playerErrorNotifier, eventHandler) + is ICantPlayCommand -> run(gameState, playerErrorNotifier, eventHandler) } } } + +fun errorNotifier(channel: SendChannel): suspend (String) -> Unit = + { + val logger = KotlinLogging.logger { } + val notification = ErrorNotification(message = it) + logger.atWarn { + message = "Notification send ERROR: ${notification.message}" + payload = mapOf("notification" to notification) + } + channel.send(notification) + } diff --git a/src/main/kotlin/eventDemo/app/command/GameCommandRoute.kt b/src/main/kotlin/eventDemo/app/command/GameCommandRoute.kt index b969191..ffe11ab 100644 --- a/src/main/kotlin/eventDemo/app/command/GameCommandRoute.kt +++ b/src/main/kotlin/eventDemo/app/command/GameCommandRoute.kt @@ -2,6 +2,7 @@ package eventDemo.app.command import eventDemo.app.entity.Player import eventDemo.app.eventListener.GameEventPlayerNotificationListener +import eventDemo.app.notification.Notification import eventDemo.libs.fromFrameChannel import eventDemo.libs.toObjectChannel import io.ktor.server.application.ApplicationCall @@ -12,6 +13,7 @@ import io.ktor.server.routing.Route import io.ktor.server.websocket.webSocket import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.launch @DelicateCoroutinesApi @@ -22,14 +24,15 @@ fun Route.gameSocket( authenticate { webSocket("/game") { val currentPlayer = call.getPlayer() + val outgoingFrameChannel: SendChannel = fromFrameChannel(outgoing) GlobalScope.launch { commandHandler.handle( currentPlayer, toObjectChannel(incoming), - fromFrameChannel(outgoing), + outgoingFrameChannel, ) } - playerNotificationListener.startListening(outgoing, currentPlayer) + playerNotificationListener.startListening(outgoingFrameChannel, currentPlayer) } } } diff --git a/src/main/kotlin/eventDemo/app/event/projection/GameStateRepository.kt b/src/main/kotlin/eventDemo/app/event/projection/GameStateRepository.kt index e262e07..e71140b 100644 --- a/src/main/kotlin/eventDemo/app/event/projection/GameStateRepository.kt +++ b/src/main/kotlin/eventDemo/app/event/projection/GameStateRepository.kt @@ -12,10 +12,10 @@ class GameStateRepository( eventHandler: GameEventHandler, private val maxSnapshotCacheSize: Int = 20, ) { - private val projections: MutableMap = ConcurrentHashMap() + private val projections: ConcurrentHashMap = ConcurrentHashMap() private val version: AtomicInteger = AtomicInteger(0) - private val projectionsSnapshot: MutableMap = ConcurrentHashMap() - private val sortedSnapshotByVersion: MutableMap = ConcurrentHashMap() + private val projectionsSnapshot: ConcurrentHashMap = ConcurrentHashMap() + private val sortedSnapshotByVersion: ConcurrentHashMap = ConcurrentHashMap() init { eventHandler.registerProjectionBuilder { event -> @@ -68,8 +68,9 @@ class GameStateRepository( * It fetches it from the local cache if possible, otherwise it builds it. */ fun get(gameId: GameId): GameState = - projections[gameId] - ?: gameId.buildStateFromEventStream(eventStream) + projections.computeIfAbsent(gameId) { + gameId.buildStateFromEventStream(eventStream) + } /** * Get the [GameState] to the specific [event][GameEvent]. @@ -78,8 +79,9 @@ class GameStateRepository( * It fetches it from the local cache if possible, otherwise it builds it. */ fun getUntil(event: GameEvent): GameState = - projectionsSnapshot[event] - ?: event.buildStateFromEventStreamTo(eventStream) + projectionsSnapshot.computeIfAbsent(event) { + event.buildStateFromEventStreamTo(eventStream) + } private fun GameState.update() { projections[gameId] = this diff --git a/src/main/kotlin/eventDemo/app/eventListener/GameEventPlayerNotificationListener.kt b/src/main/kotlin/eventDemo/app/eventListener/GameEventPlayerNotificationListener.kt index 2808d40..0dba1f4 100644 --- a/src/main/kotlin/eventDemo/app/eventListener/GameEventPlayerNotificationListener.kt +++ b/src/main/kotlin/eventDemo/app/eventListener/GameEventPlayerNotificationListener.kt @@ -23,9 +23,7 @@ import eventDemo.app.notification.PlayerWinNotification import eventDemo.app.notification.TheGameWasStartedNotification import eventDemo.app.notification.WelcomeToTheGameNotification import eventDemo.app.notification.YourNewCardNotification -import eventDemo.shared.toFrame import io.github.oshai.kotlinlogging.KotlinLogging -import io.ktor.websocket.Frame import kotlinx.coroutines.channels.SendChannel import kotlinx.coroutines.channels.trySendBlocking @@ -36,7 +34,7 @@ class GameEventPlayerNotificationListener( private val logger = KotlinLogging.logger {} fun startListening( - outgoingNotificationChannel: SendChannel, + outgoingNotificationChannel: SendChannel, currentPlayer: Player, ) { eventBus.subscribe { event: GameEvent -> @@ -45,7 +43,7 @@ class GameEventPlayerNotificationListener( fun Notification.send() { if (currentState.players.contains(currentPlayer)) { // Only notify players who have already joined the game. - outgoingNotificationChannel.trySendBlocking(toFrame()) + outgoingNotificationChannel.trySendBlocking(this) logger.atInfo { message = "Notification for player ${currentPlayer.name} was SEND: ${this@send}" payload = mapOf("notification" to this@send, "event" to event) diff --git a/src/main/kotlin/eventDemo/configuration/ConfigureDI.kt b/src/main/kotlin/eventDemo/configuration/ConfigureDI.kt index 84808bd..117780f 100644 --- a/src/main/kotlin/eventDemo/configuration/ConfigureDI.kt +++ b/src/main/kotlin/eventDemo/configuration/ConfigureDI.kt @@ -1,11 +1,13 @@ package eventDemo.configuration import eventDemo.app.command.GameCommandHandler +import eventDemo.app.command.command.GameCommand import eventDemo.app.event.GameEventBus import eventDemo.app.event.GameEventHandler import eventDemo.app.event.GameEventStream import eventDemo.app.event.projection.GameStateRepository import eventDemo.app.eventListener.GameEventPlayerNotificationListener +import eventDemo.libs.command.CommandStreamChannelBuilder import eventDemo.libs.event.EventBusInMemory import eventDemo.libs.event.EventStreamInMemory import io.ktor.server.application.Application @@ -33,6 +35,9 @@ val appKoinModule = single { GameStateRepository(get(), get()) } + single { + CommandStreamChannelBuilder() + } singleOf(::GameEventHandler) singleOf(::GameCommandHandler) diff --git a/src/main/kotlin/eventDemo/libs/command/CommandStreamChannel.kt b/src/main/kotlin/eventDemo/libs/command/CommandStreamChannel.kt index 271c371..a2f2658 100644 --- a/src/main/kotlin/eventDemo/libs/command/CommandStreamChannel.kt +++ b/src/main/kotlin/eventDemo/libs/command/CommandStreamChannel.kt @@ -2,18 +2,48 @@ package eventDemo.libs.command import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes + +class CommandStreamChannelBuilder( + private val maxCacheTime: Duration = 10.minutes, +) { + operator fun invoke(incoming: ReceiveChannel): CommandStreamChannel = CommandStreamChannel(incoming, maxCacheTime) +} /** * Manage [Command]'s with kotlin Channel */ class CommandStreamChannel( private val incoming: ReceiveChannel, + private val maxCacheTime: Duration = 10.minutes, ) : CommandStream { private val logger = KotlinLogging.logger {} + private val executedCommand: ConcurrentHashMap> = ConcurrentHashMap() override suspend fun process(action: CommandBlock) { for (command in incoming) { - compute(command, action) + val now = Clock.System.now() + val (status, _) = executedCommand.computeIfAbsent(command.id) { Pair(false, now) } + + if (status) { + logger.atWarn { + message = "Command already executed: $command" + payload = mapOf("command" to command) + } + } else { + compute(command, action) + } + executedCommand + .filterValues { (_, date) -> + (date + maxCacheTime) > now + }.keys + .forEach { + executedCommand.remove(it) + } } } diff --git a/src/main/kotlin/eventDemo/libs/event/EventStreamInMemory.kt b/src/main/kotlin/eventDemo/libs/event/EventStreamInMemory.kt index 7edc751..9f9f9bf 100644 --- a/src/main/kotlin/eventDemo/libs/event/EventStreamInMemory.kt +++ b/src/main/kotlin/eventDemo/libs/event/EventStreamInMemory.kt @@ -36,7 +36,10 @@ class EventStreamInMemory, ID : AggregateId> : EventStream .filterIsInstance(eventType.java) .lastOrNull { it.gameId == aggregateId } - override fun readAll(aggregateId: ID): Set = events.toSet() + override fun readAll(aggregateId: ID): Set = + events + .filter { it.gameId == aggregateId } + .toSet() } inline fun , ID : AggregateId> EventStream.readLastOf(aggregateId: ID): R? = diff --git a/src/main/kotlin/eventDemo/shared/FrameConverter.kt b/src/main/kotlin/eventDemo/shared/FrameConverter.kt deleted file mode 100644 index 0162c01..0000000 --- a/src/main/kotlin/eventDemo/shared/FrameConverter.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eventDemo.shared - -import eventDemo.app.command.command.GameCommand -import eventDemo.app.event.event.GameEvent -import eventDemo.app.notification.Notification -import io.ktor.websocket.Frame -import io.ktor.websocket.readText -import kotlinx.serialization.json.Json - -fun Frame.Text.toEvent(): GameEvent = Json.decodeFromString(GameEvent.serializer(), readText()) - -fun GameEvent.toFrame(): Frame.Text = Frame.Text(Json.encodeToString(GameEvent.serializer(), this)) - -fun Frame.Text.toCommand(): GameCommand = Json.decodeFromString(GameCommand.serializer(), readText()) - -fun GameCommand.toFrame(): Frame.Text = Frame.Text(Json.encodeToString(GameCommand.serializer(), this)) - -fun Frame.toNotification(): Notification = - Json.decodeFromString( - Notification.serializer(), - (this as Frame.Text).readText(), - ) - -fun Notification.toFrame(): Frame.Text = Frame.Text(Json.encodeToString(Notification.serializer(), this)) diff --git a/src/test/kotlin/eventDemo/app/command/GameCommandHandlerTest.kt b/src/test/kotlin/eventDemo/app/command/GameCommandHandlerTest.kt new file mode 100644 index 0000000..d11274a --- /dev/null +++ b/src/test/kotlin/eventDemo/app/command/GameCommandHandlerTest.kt @@ -0,0 +1,43 @@ +package eventDemo.app.command + +import eventDemo.app.command.command.GameCommand +import eventDemo.app.command.command.IWantToJoinTheGameCommand +import eventDemo.app.entity.GameId +import eventDemo.app.entity.Player +import eventDemo.app.eventListener.GameEventPlayerNotificationListener +import eventDemo.app.eventListener.GameEventReactionListener +import eventDemo.app.notification.Notification +import eventDemo.app.notification.WelcomeToTheGameNotification +import eventDemo.configuration.appKoinModule +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContain +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import org.koin.dsl.koinApplication +import kotlin.test.assertIs + +class GameCommandHandlerTest : + FunSpec({ + test("handle a command should execute the command") { + koinApplication { modules(appKoinModule) }.koin.apply { + val commandHandler by inject() + val notificationListener by inject() + val gameId = GameId() + val player = Player("Tesla") + val channelCommand = Channel(Channel.BUFFERED) + val channelNotification = Channel(Channel.BUFFERED) + GameEventReactionListener(get(), get(), get()).init() + notificationListener.startListening(channelNotification, player) + + GlobalScope.launch { + commandHandler.handle(player, channelCommand, channelNotification) + } + + channelCommand.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player))) + assertIs(channelNotification.receive()).let { + it.players shouldContain player + } + } + } + }) diff --git a/src/test/kotlin/eventDemo/app/command/command/ICantPlayCommandTest.kt b/src/test/kotlin/eventDemo/app/command/command/ICantPlayCommandTest.kt new file mode 100644 index 0000000..0711aab --- /dev/null +++ b/src/test/kotlin/eventDemo/app/command/command/ICantPlayCommandTest.kt @@ -0,0 +1,9 @@ +package eventDemo.app.command.command + +import io.kotest.core.spec.style.FunSpec + +class ICantPlayCommandTest : + FunSpec({ + + xtest("run should publish the event") { } + }) diff --git a/src/test/kotlin/eventDemo/app/command/command/IWantToJoinTheGameCommandTest.kt b/src/test/kotlin/eventDemo/app/command/command/IWantToJoinTheGameCommandTest.kt new file mode 100644 index 0000000..ecd24ba --- /dev/null +++ b/src/test/kotlin/eventDemo/app/command/command/IWantToJoinTheGameCommandTest.kt @@ -0,0 +1,9 @@ +package eventDemo.app.command.command + +import io.kotest.core.spec.style.FunSpec + +class IWantToJoinTheGameCommandTest : + FunSpec({ + + xtest("run should publish the event") { } + }) diff --git a/src/test/kotlin/eventDemo/app/command/command/IWantToPlayCardCommandTest.kt b/src/test/kotlin/eventDemo/app/command/command/IWantToPlayCardCommandTest.kt new file mode 100644 index 0000000..e8ffe1d --- /dev/null +++ b/src/test/kotlin/eventDemo/app/command/command/IWantToPlayCardCommandTest.kt @@ -0,0 +1,9 @@ +package eventDemo.app.command.command + +import io.kotest.core.spec.style.FunSpec + +class IWantToPlayCardCommandTest : + FunSpec({ + + xtest("run should publish the event") { } + }) diff --git a/src/test/kotlin/eventDemo/app/command/command/IamReadyToPlayCommandTest.kt b/src/test/kotlin/eventDemo/app/command/command/IamReadyToPlayCommandTest.kt new file mode 100644 index 0000000..99cbc3e --- /dev/null +++ b/src/test/kotlin/eventDemo/app/command/command/IamReadyToPlayCommandTest.kt @@ -0,0 +1,9 @@ +package eventDemo.app.command.command + +import io.kotest.core.spec.style.FunSpec + +class IamReadyToPlayCommandTest : + FunSpec({ + + xtest("run should publish the event") { } + }) diff --git a/src/test/kotlin/eventDemo/app/entity/DeckTest.kt b/src/test/kotlin/eventDemo/app/entity/DeckTest.kt index 01f8d76..1ebb251 100644 --- a/src/test/kotlin/eventDemo/app/entity/DeckTest.kt +++ b/src/test/kotlin/eventDemo/app/entity/DeckTest.kt @@ -24,7 +24,7 @@ class DeckTest : deck.allCards().map { it.id }.shouldBeUnique() } - test("initHands") { + test("initHands should be generate the hands of all players from the stack") { // Given val playerNumbers = 4 val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() @@ -41,7 +41,7 @@ class DeckTest : initDeck.allCardCount() shouldBeExactly totalCardsNumber } - test("takeOneCardFromStackTo") { + test("takeOneCardFromStackTo player") { // Given val playerNumbers = 4 val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() @@ -89,7 +89,6 @@ class DeckTest : val playerNumbers = 4 val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet() val deck = Deck.newWithoutPlayers().initHands(players) - val firstPlayer = players.first() // When val modifiedDeck = deck.placeFirstCardOnDiscard() diff --git a/src/test/kotlin/eventDemo/app/entity/PlayersHandsTest.kt b/src/test/kotlin/eventDemo/app/entity/PlayersHandsTest.kt new file mode 100644 index 0000000..72aed5a --- /dev/null +++ b/src/test/kotlin/eventDemo/app/entity/PlayersHandsTest.kt @@ -0,0 +1,15 @@ +package eventDemo.app.entity + +import io.kotest.core.spec.style.FunSpec + +class PlayersHandsTest : + FunSpec({ + + xtest("getHand should return the hand of the player") { } + + xtest("removeCard should remove the card") { } + + xtest("addCard should add the card to the correct hand") { } + + xtest("toPlayersHands should build object from map") { } + }) diff --git a/src/test/kotlin/eventDemo/app/event/GameEventHandlerTest.kt b/src/test/kotlin/eventDemo/app/event/GameEventHandlerTest.kt new file mode 100644 index 0000000..0705ecd --- /dev/null +++ b/src/test/kotlin/eventDemo/app/event/GameEventHandlerTest.kt @@ -0,0 +1,10 @@ +package eventDemo.app.event + +import io.kotest.core.spec.style.FunSpec + +class GameEventHandlerTest : + FunSpec({ + xtest("handle event should publish the event to the stream") { } + xtest("handle event should build the registered projection") { } + xtest("handle event should publish the event to the bus") { } + }) diff --git a/src/test/kotlin/eventDemo/app/event/projection/GameStateRepositoryTest.kt b/src/test/kotlin/eventDemo/app/event/projection/GameStateRepositoryTest.kt new file mode 100644 index 0000000..4a93e64 --- /dev/null +++ b/src/test/kotlin/eventDemo/app/event/projection/GameStateRepositoryTest.kt @@ -0,0 +1,16 @@ +package eventDemo.app.event.projection + +import io.kotest.core.spec.style.FunSpec + +class GameStateRepositoryTest : + FunSpec({ + xtest("GameStateRepository should build the projection when a new event occurs") { } + + xtest("get should build the last version of the state") { } + xtest("get should be concurrently secure") { } + xtest("get should be concurrently secure") { } + + xtest("getUntil should build the state until the event") { } + xtest("call getUntil twice should get the state from the cache") { } + xtest("getUntil should be concurrently secure") { } + }) diff --git a/src/test/kotlin/eventDemo/app/event/projection/GameStateTest.kt b/src/test/kotlin/eventDemo/app/event/projection/GameStateTest.kt new file mode 100644 index 0000000..1738ad9 --- /dev/null +++ b/src/test/kotlin/eventDemo/app/event/projection/GameStateTest.kt @@ -0,0 +1,15 @@ +package eventDemo.app.event.projection + +import io.kotest.core.spec.style.FunSpec + +class GameStateTest : + FunSpec({ + xtest("isReady") { } + xtest("nextPlayer") { } + xtest("nextPlayerTurn") { } + xtest("playerDiffIndex") { } + xtest("cardOnBoardIsForYou") { } + xtest("playableCards") { } + xtest("playerHasNoCardLeft") { } + xtest("canBePlayThisCard") { } + }) diff --git a/src/test/kotlin/eventDemo/app/query/GameStateRouteTest.kt b/src/test/kotlin/eventDemo/app/query/GameStateRouteTest.kt index d89f3a1..4d4b8e3 100644 --- a/src/test/kotlin/eventDemo/app/query/GameStateRouteTest.kt +++ b/src/test/kotlin/eventDemo/app/query/GameStateRouteTest.kt @@ -50,7 +50,7 @@ class GameStateRouteTest : }.apply { assertEquals(HttpStatusCode.OK, status, message = bodyAsText()) val state = call.body() - assertEquals(id, state.gameId) + id shouldBeEqual state.gameId state.players shouldHaveSize 0 state.isStarted shouldBeEqual false } diff --git a/src/test/kotlin/eventDemo/app/query/GameStateTest.kt b/src/test/kotlin/eventDemo/app/query/GameStateTest.kt index 8bd8af3..49281b6 100644 --- a/src/test/kotlin/eventDemo/app/query/GameStateTest.kt +++ b/src/test/kotlin/eventDemo/app/query/GameStateTest.kt @@ -1,6 +1,7 @@ package eventDemo.app.query import eventDemo.app.command.GameCommandHandler +import eventDemo.app.command.command.GameCommand import eventDemo.app.command.command.IWantToJoinTheGameCommand import eventDemo.app.command.command.IWantToPlayCardCommand import eventDemo.app.command.command.IamReadyToPlayCommand @@ -14,20 +15,16 @@ import eventDemo.app.event.projection.buildStateFromEventStream import eventDemo.app.eventListener.GameEventPlayerNotificationListener import eventDemo.app.eventListener.GameEventReactionListener import eventDemo.app.notification.ItsTheTurnOfNotification +import eventDemo.app.notification.Notification import eventDemo.app.notification.PlayerAsJoinTheGameNotification import eventDemo.app.notification.PlayerAsPlayACardNotification import eventDemo.app.notification.PlayerWasReadyNotification import eventDemo.app.notification.TheGameWasStartedNotification import eventDemo.app.notification.WelcomeToTheGameNotification import eventDemo.configuration.appKoinModule -import eventDemo.libs.fromFrameChannel -import eventDemo.libs.toObjectChannel -import eventDemo.shared.toFrame -import eventDemo.shared.toNotification import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.equals.shouldBeEqual -import io.ktor.websocket.Frame import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -48,46 +45,46 @@ class GameStateTest : val id = GameId() val player1 = Player(name = "Nikola") val player2 = Player(name = "Einstein") - val channelCommand1 = Channel(Channel.BUFFERED) - val channelCommand2 = Channel(Channel.BUFFERED) - val channelNotification1 = Channel(Channel.BUFFERED) - val channelNotification2 = Channel(Channel.BUFFERED) + val channelCommand1 = Channel(Channel.BUFFERED) + val channelCommand2 = Channel(Channel.BUFFERED) + val channelNotification1 = Channel(Channel.BUFFERED) + val channelNotification2 = Channel(Channel.BUFFERED) var playedCard1: Card? = null var playedCard2: Card? = null val player1Job = launch { - channelCommand1.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player1)).toFrame()) - channelNotification1.receive().toNotification().let { + channelCommand1.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player1))) + channelNotification1.receive().let { assertIs(it).players shouldBeEqual setOf(player1) } - channelNotification1.receive().toNotification().let { + channelNotification1.receive().let { assertIs(it).player shouldBeEqual player2 } - channelCommand1.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player1)).toFrame()) - channelNotification1.receive().toNotification().let { + channelCommand1.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player1))) + channelNotification1.receive().let { assertIs(it).player shouldBeEqual player2 } val player1Hand = - channelNotification1.receive().toNotification().let { + channelNotification1.receive().let { assertIs(it).hand shouldHaveSize 7 } playedCard1 = player1Hand.first() - channelNotification1.receive().toNotification().let { + channelNotification1.receive().let { assertIs(it).apply { player shouldBeEqual player1 } } - channelCommand1.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player1, player1Hand.first())).toFrame()) + channelCommand1.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player1, player1Hand.first()))) - channelNotification1.receive().toNotification().let { + channelNotification1.receive().let { assertIs(it).apply { player shouldBeEqual player2 } } - channelNotification1.receive().toNotification().let { + channelNotification1.receive().let { assertIs(it).apply { player shouldBeEqual player2 card shouldBeEqual assertNotNull(playedCard2) @@ -98,24 +95,24 @@ class GameStateTest : val player2Job = launch { delay(100) - channelCommand2.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player2)).toFrame()) - channelNotification2.receive().toNotification().let { + channelCommand2.send(IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(id, player2))) + channelNotification2.receive().let { assertIs(it).players shouldBeEqual setOf(player1, player2) } - channelNotification2.receive().toNotification().let { + channelNotification2.receive().let { assertIs(it).player shouldBeEqual player1 } - channelCommand2.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player2)).toFrame()) + channelCommand2.send(IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(id, player2))) val player2Hand = - channelNotification2.receive().toNotification().let { + channelNotification2.receive().let { assertIs(it).hand shouldHaveSize 7 } - channelNotification2.receive().toNotification().let { + channelNotification2.receive().let { assertIs(it).apply { player shouldBeEqual player1 } } - channelNotification2.receive().toNotification().let { + channelNotification2.receive().let { assertIs(it).apply { player shouldBeEqual player1 card shouldBeEqual assertNotNull(playedCard1) @@ -123,12 +120,12 @@ class GameStateTest : } playedCard2 = player2Hand.first() - channelNotification2.receive().toNotification().let { + channelNotification2.receive().let { assertIs(it).apply { player shouldBeEqual player2 } } - channelCommand2.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player2, player2Hand.first())).toFrame()) + channelCommand2.send(IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(id, player2, player2Hand.first()))) } koinApplication { modules(appKoinModule) }.koin.apply { @@ -140,10 +137,10 @@ class GameStateTest : playerNotificationListener.startListening(channelNotification2, player2) GlobalScope.launch(Dispatchers.IO) { - commandHandler.handle(player1, toObjectChannel(channelCommand1), fromFrameChannel(channelNotification1)) + commandHandler.handle(player1, channelCommand1, channelNotification1) } GlobalScope.launch(Dispatchers.IO) { - commandHandler.handle(player2, toObjectChannel(channelCommand2), fromFrameChannel(channelNotification2)) + commandHandler.handle(player2, channelCommand2, channelNotification2) } joinAll(player1Job, player2Job) diff --git a/src/test/kotlin/eventDemo/libs/event/EventBusInMemoryTest.kt b/src/test/kotlin/eventDemo/libs/event/EventBusInMemoryTest.kt new file mode 100644 index 0000000..6e4483f --- /dev/null +++ b/src/test/kotlin/eventDemo/libs/event/EventBusInMemoryTest.kt @@ -0,0 +1,10 @@ +package eventDemo.libs.event + +import io.kotest.core.spec.style.FunSpec + +class EventBusInMemoryTest : + FunSpec({ + + xtest("publish should call the subscribed functions") { } + xtest("publish should call the subscribed functions on the priority order") { } + }) diff --git a/src/test/kotlin/eventDemo/libs/event/EventStreamInMemoryTest.kt b/src/test/kotlin/eventDemo/libs/event/EventStreamInMemoryTest.kt new file mode 100644 index 0000000..d85c282 --- /dev/null +++ b/src/test/kotlin/eventDemo/libs/event/EventStreamInMemoryTest.kt @@ -0,0 +1,16 @@ +package eventDemo.libs.event + +import io.kotest.core.spec.style.FunSpec + +class EventStreamInMemoryTest : + FunSpec({ + + xtest("publish should be concurrently secure") { } + + xtest("readLast should only return the event of aggregate") { } + xtest("readLast should return the last event of the aggregate") { } + + xtest("readLastOf should return the last event of the aggregate of the type") { } + + xtest("readAll should only return the event of aggregate") { } + })