refactoring: Masive refactor to build the V2
Tests / build (push) Successful in 7m14s
Tests / lint (push) Successful in 8m35s
Tests / test (push) Failing after 11m0s

This commit is contained in:
2026-07-30 23:01:50 +02:00
parent b313b39cf4
commit e2d7942c7e
260 changed files with 5049 additions and 4753 deletions
@@ -1,48 +0,0 @@
package eventDemo.adapter.infrastructure.event
import com.rabbitmq.client.ConnectionFactory
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.event.GameEventBus
import eventDemo.domain.event.event.NewPlayerEvent
import eventDemo.testKoinApplicationWithConfig
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.core.spec.style.FunSpec
import io.kotest.datatest.withData
import io.kotest.matchers.equals.shouldBeEqual
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import java.util.function.Function
import kotlin.time.Duration.Companion.seconds
class GameEventBusInRabbitMQTest :
FunSpec({
context("Pub/sub") {
testKoinApplicationWithConfig {
val busListToTest: Map<String, GameEventBus> =
mapOf(
GameEventBusInMemory::class.java.simpleName to GameEventBusInMemory(),
GameEventBusInRabbinMQ::class.java.simpleName to GameEventBusInRabbinMQ(get<ConnectionFactory>()),
)
withData(busListToTest) { bus ->
val spy = spyk<() -> Unit>()
val aggregateId = GameId()
val player1 = Player(name = "Tesla")
val player2 = Player(name = "Einstein")
bus.subscribe { obj ->
spy()
obj.aggregateId shouldBeEqual aggregateId
}
bus.publish(NewPlayerEvent(aggregateId, player1, 1))
bus.publish(NewPlayerEvent(aggregateId, player2, 2))
eventually(1.seconds) {
verify(exactly = 2) { spy() }
}
}
}
}
})
@@ -1,10 +0,0 @@
package eventDemo.adapter.presenter.query
import eventDemo.domain.entity.Player
import eventDemo.configuration.ktor.makeJwt
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.header
internal fun HttpRequestBuilder.withAuth(player: Player) {
header("Authorization", "Bearer ${player.makeJwt("secret")}")
}
@@ -1,115 +0,0 @@
package eventDemo.adapter.presenter.query
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.event.GameEventHandler
import eventDemo.domain.event.event.GameStartedEvent
import eventDemo.domain.event.event.NewPlayerEvent
import eventDemo.domain.event.event.PlayerReadyEvent
import eventDemo.domain.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
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.equals.shouldBeEqual
import io.ktor.client.call.body
import io.ktor.client.request.accept
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
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)
accept(ContentType.Application.Json)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
val list = call.body<List<GameList>>()
assertTrue(list.isEmpty())
}
}
}
test("/games return a game with status OPENING") {
val gameId = GameId()
val player1 = Player(name = "Nikola")
testApplicationWithConfig(
{
get<GameEventHandler>()
.handle(gameId) {
NewPlayerEvent(gameId, player1, it)
}
},
) {
// Wait until the projection is created
eventually(10.seconds) {
httpClient()
.get("/games") {
withAuth(player1)
accept(ContentType.Application.Json)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
call.body<List<GameList>>().first().let {
it.status shouldBeEqual GameList.Status.OPENING
it.players shouldHaveSize 1
it.players shouldContain player1
it.winners shouldHaveSize 0
}
}
}
}
}
test("/games return a game with status IS_STARTED") {
val gameId = GameId()
val player1 = Player(name = "Nikola")
val player2 = Player(name = "Einstein")
testApplicationWithConfig({
val eventHandler = get<GameEventHandler>()
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(3.seconds) {
httpClient()
.get("/games") {
withAuth(player1)
accept(ContentType.Application.Json)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
call.body<List<GameList>>().first().let {
it.status shouldBeEqual GameList.Status.IS_STARTED
it.players shouldHaveSize 2
it.players shouldContain player1
it.players shouldContain player2
it.winners shouldHaveSize 0
}
}
}
}
}
})
@@ -1,196 +0,0 @@
package eventDemo.adapter.presenter.query
import eventDemo.Tag
import eventDemo.domain.command.GameCommandHandler
import eventDemo.domain.command.command.GameCommand
import eventDemo.domain.command.command.IWantToJoinTheGameCommand
import eventDemo.domain.command.command.IWantToPlayCardCommand
import eventDemo.domain.command.command.IamReadyToPlayCommand
import eventDemo.domain.entity.Card
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.event.event.disableShuffleDeck
import eventDemo.domain.event.projection.GameState
import eventDemo.domain.event.projection.GameStateRepository
import eventDemo.domain.event.projection.projectionListener.PlayerNotificationListener
import eventDemo.domain.notification.CommandSuccessNotification
import eventDemo.domain.notification.ItsTheTurnOfNotification
import eventDemo.domain.notification.Notification
import eventDemo.domain.notification.PlayerAsJoinTheGameNotification
import eventDemo.domain.notification.PlayerAsPlayACardNotification
import eventDemo.domain.notification.PlayerWasReadyNotification
import eventDemo.domain.notification.TheGameWasStartedNotification
import eventDemo.domain.notification.WelcomeToTheGameNotification
import eventDemo.testKoinApplicationWithConfig
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.assertions.nondeterministic.until
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
@DelicateCoroutinesApi
class GameSimulationTest :
FunSpec({
tags(Tag.Postgresql)
test("Simulation of a game") {
withTimeout(10.seconds) {
disableShuffleDeck()
val gameId = GameId()
val player1 = Player(name = "Nikola")
val player2 = Player(name = "Einstein")
val channelCommand1 = Channel<GameCommand>(Channel.BUFFERED)
val channelCommand2 = Channel<GameCommand>(Channel.BUFFERED)
val channelNotification1 = Channel<Notification>(Channel.BUFFERED)
val channelNotification2 = Channel<Notification>(Channel.BUFFERED)
var playedCard1: Card? = null
var playedCard2: Card? = null
var player1HasJoin = false
testKoinApplicationWithConfig {
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
run {
GlobalScope.launch(Dispatchers.IO) {
commandHandler.handleIncomingPlayerCommands(player1, gameId, channelCommand1, channelNotification1)
}
GlobalScope.launch(Dispatchers.IO) {
commandHandler.handleIncomingPlayerCommands(player2, gameId, channelCommand2, channelNotification2)
}
}
// 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 {
GlobalScope.launch {
for (notification in channelNotification1) {
player1Notifications.add(notification)
}
}
GlobalScope.launch {
for (notification in channelNotification2) {
player2Notifications.add(notification)
}
}
}
// Player 1 actions
val player1Job =
launch {
playerNotificationListener.startListening(player1, gameId) {
channelNotification1.trySendBlocking(it)
}
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player1)).also { sendCommand ->
channelCommand1.send(sendCommand)
player1Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
}
player1HasJoin = true
player1Notifications.waitNotification<WelcomeToTheGameNotification> { players == setOf(player1) }
player1Notifications.waitNotification<PlayerAsJoinTheGameNotification> { player == player2 }
IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(gameId, player1)).also { sendCommand ->
channelCommand1.send(sendCommand)
player1Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
}
player1Notifications.waitNotification<PlayerWasReadyNotification> { player == player2 }
val player1Hand = player1Notifications.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }.hand
playedCard1 = player1Hand.first()
player1Notifications.waitNotification<ItsTheTurnOfNotification> { player == player1 }
IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(gameId, player1, player1Hand.first())).also { sendCommand ->
channelCommand1.send(sendCommand)
player1Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
}
player1Notifications.waitNotification<ItsTheTurnOfNotification> { player == player2 }
player1Notifications.waitNotification<PlayerAsPlayACardNotification> {
player == player2 && card == playedCard2
}
}
// Player 2 actions
val player2Job =
launch {
// wait player 1 has joined the game
until(3.seconds) { player1HasJoin }
playerNotificationListener.startListening(player2, gameId) {
channelNotification2.trySendBlocking(it)
}
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player2)).also { sendCommand ->
channelCommand2.send(sendCommand)
player2Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
}
player2Notifications.waitNotification<WelcomeToTheGameNotification> { players == setOf(player1, player2) }
player2Notifications.waitNotification<PlayerWasReadyNotification> { player == player1 }
IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(gameId, player2)).also { sendCommand ->
channelCommand2.send(sendCommand)
player2Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
}
val player2Hand =
player2Notifications.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }.hand
player2Notifications.waitNotification<ItsTheTurnOfNotification> { player == player1 }
player2Notifications.waitNotification<PlayerAsPlayACardNotification> {
player == player1 && card == playedCard1
}
playedCard2 = player2Hand.first()
player2Notifications.waitNotification<ItsTheTurnOfNotification> { player == player2 }
IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(gameId, player2, player2Hand.first())).also { sendCommand ->
channelCommand2.send(sendCommand)
player2Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
}
}
// Wait the end of the game
joinAll(player1Job, player2Job)
// Build the last state from the event store
val state = gameStateRepository.get(gameId)
// Check if the state is correct
state.aggregateId shouldBeEqual gameId
assertTrue(state.isStarted)
state.players shouldBeEqual setOf(player1, player2)
state.readyPlayers shouldBeEqual setOf(player1, player2)
state.direction shouldBeEqual GameState.Direction.CLOCKWISE
assertNotNull(state.lastCardPlayer) shouldBeEqual player2
assertNotNull(state.cardOnCurrentStack) shouldBeEqual assertNotNull(playedCard2)
}
}
}
})
private suspend inline fun <reified T : Notification> MutableList<Notification>.waitNotification(crossinline block: T.() -> Boolean): T =
eventually(3.seconds) {
filterIsInstance<T>().first { block(it) }
}
@@ -1,157 +0,0 @@
package eventDemo.adapter.presenter.query
import eventDemo.domain.entity.Card
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.event.GameEventHandler
import eventDemo.domain.event.event.CardIsPlayedEvent
import eventDemo.domain.event.event.NewPlayerEvent
import eventDemo.domain.event.event.PlayerReadyEvent
import eventDemo.domain.event.event.disableShuffleDeck
import eventDemo.domain.event.projection.GameState
import eventDemo.domain.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
import io.ktor.client.call.body
import io.ktor.client.request.accept
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.assertIs
import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds
class GameStateRouteTest :
FunSpec({
test("""The route "/games/{id}/state" should returns a non started game when is not exist""") {
testApplicationWithConfig {
val gameId = GameId()
val player1 = Player(name = "Nikola")
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 0
isStarted shouldBeEqual false
}
}
}
}
test("""The route "/games/{id}/state" should returns the state with all informations""") {
val gameId = GameId()
val player1 = Player(name = "Nikola")
val player2 = Player(name = "Einstein")
testApplicationWithConfig({
disableShuffleDeck()
val eventHandler = get<GameEventHandler>()
val stateRepo = get<GameStateRepository>()
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) }
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
}
eventHandler.handle(gameId) {
CardIsPlayedEvent(
gameId,
lastPlayedCard,
player1,
it,
)
}
until(3.seconds) {
stateRepo
.get(gameId)
.deck.discard
.last() == lastPlayedCard
}
}
}) {
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
}
}
}
}
test("""The route "/games/{id}/card/last" should return the last card played of the game""") {
val gameId = GameId()
val player1 = Player(name = "Nikola")
val player2 = Player(name = "Einstein")
var lastPlayedCard: Card? = null
testApplicationWithConfig({
disableShuffleDeck()
val eventHandler = get<GameEventHandler>()
val stateRepo = get<GameStateRepository>()
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) }
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
}
eventHandler.handle(gameId) {
CardIsPlayedEvent(
gameId,
assertNotNull(lastPlayedCard),
player1,
it,
)
}
until(3.seconds) {
stateRepo
.get(gameId)
.deck.discard
.last() == lastPlayedCard
}
}
}) {
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>())
}
}
}
})
@@ -0,0 +1,42 @@
package eventDemo.architecture
import com.tngtech.archunit.core.importer.ClassFileImporter
import com.tngtech.archunit.core.importer.ImportOption
import com.tngtech.archunit.library.Architectures.layeredArchitecture
import org.junit.jupiter.api.Test
/**
* Vérifie le respect des frontières de l'architecture hexagonale (ports & adapters).
*
* Convention attendue :
* eventDemo.contexts.uno.domain
* eventDemo.contexts.uno.application
* eventDemo.contexts.uno.infrastructure
*
* Règles imposées :
* domain → ne dépend d'aucune autre couche (ni application, ni infrastructure)
* application → ne dépend que de domain (jamais d'infrastructure)
* infrastructure → ne dépend que de domain et application
*/
class HexagonalArchitectureTest {
private val basePackage = "eventDemo.contexts.uno"
private val classes =
ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages(basePackage)
@Test
fun `respecte les couches de l'architecture hexagonale`() {
@Suppress("ktlint:standard:chain-method-continuation")
layeredArchitecture()
.consideringAllDependencies()
.layer("Domain").definedBy("$basePackage.domain..")
.layer("Application").definedBy("$basePackage.application..")
.layer("Infrastructure").definedBy("$basePackage.infrastructure..")
.whereLayer("Domain").mayNotAccessAnyLayer()
.whereLayer("Application").mayOnlyAccessLayers("Domain")
.whereLayer("Infrastructure").mayOnlyAccessLayers("Domain", "Application")
.check(classes)
}
}
@@ -0,0 +1,34 @@
package eventDemo.contexts.auth.domain
import eventDemo.contexts.auth.domain.events.NewUserCreatedEvent
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.assertNotNull
class UserTest :
FunSpec({
test("Create User") {
User.createNewUser("Bob", "changeit").run {
username shouldBe "Bob"
password shouldBe "changeit"
}
}
test("Create User With event") {
User
.loadFromHistory(
setOf(
NewUserCreatedEvent(
"Bob",
"changeit",
version = 1,
),
),
).run {
assertNotNull(this)
username shouldBe "Bob"
password shouldBe "changeit"
}
}
})
@@ -0,0 +1,282 @@
package eventDemo.contexts.game.application
import eventDemo.Tag
import eventDemo.contexts.auth.application.eventStores.UserRepository
import eventDemo.contexts.auth.domain.User
import eventDemo.contexts.game.application.channels.GameChannelsSubscriber
import eventDemo.contexts.game.application.command.models.GameCommand
import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.notification.models.ItsTheTurnOfNotification
import eventDemo.contexts.game.application.notification.models.Notification
import eventDemo.contexts.game.application.notification.models.PlayerAsJoinTheGameNotification
import eventDemo.contexts.game.application.notification.models.PlayerAsPlayACardNotification
import eventDemo.contexts.game.application.notification.models.PlayerWasReadyNotification
import eventDemo.contexts.game.application.notification.models.TheGameWasStartedNotification
import eventDemo.contexts.game.application.notification.models.WelcomeToTheGameNotification
import eventDemo.contexts.game.domain.game.Card
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.gameState.Game
import eventDemo.contexts.game.domain.game.gameState.GameStarted
import eventDemo.contexts.game.domain.game.gameState.disableRandomForTest
import eventDemo.testHelpers.CreateGameWithCommandsInChannelsHelpers.createGameWithCommandsInChannels
import eventDemo.testHelpers.CreateGameWithCommandsInChannelsHelpers.joinTheGame
import eventDemo.testHelpers.CreateGameWithCommandsInChannelsHelpers.playCard
import eventDemo.testHelpers.CreateGameWithCommandsInChannelsHelpers.readyToPlay
import eventDemo.testHelpers.createNewUser
import eventDemo.testHelpers.testKoinApplicationWithConfig
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.assertions.nondeterministic.until
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContainExactly
import io.kotest.matchers.equals.shouldBeEqual
import io.kotest.matchers.equals.shouldEqual
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.types.shouldBeInstanceOf
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import org.junit.jupiter.api.assertInstanceOf
import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds
@DelicateCoroutinesApi
class GameSimulationTest :
FunSpec({
tags(Tag.Postgresql)
test("Simulation of a game") {
withTimeout(10.seconds) {
disableRandomForTest()
val gameId = GameId()
val user1 = createNewUser("user1")
val user2 = createNewUser("user2")
val channelCommand1 = Channel<GameCommand>(Channel.BUFFERED)
val channelCommand2 = Channel<GameCommand>(Channel.BUFFERED)
val channelNotification1 = Channel<Notification>(Channel.BUFFERED)
val channelNotification2 = Channel<Notification>(Channel.BUFFERED)
var playedCard1: Card? = null
var playedCard2: Card? = null
var player1HasJoin = false
testKoinApplicationWithConfig {
val gameRepository = get<GameRepository>()
val userRepository = get<UserRepository>()
userRepository.run {
save(user1)
save(user2)
}
gameRepository.create(gameId)
// Run command/notification subscriber
// In the normal process, these subscriber is invoque on players connect to the websocket
GlobalScope.launch(Dispatchers.IO) {
get<GameChannelsSubscriber>().subscribePlayerToGameChannels(
gameId,
user1.id,
channelCommand1,
channelNotification1,
)
}
GlobalScope.launch(Dispatchers.IO) {
get<GameChannelsSubscriber>().subscribePlayerToGameChannels(
gameId,
user2.id,
channelCommand2,
channelNotification2,
)
}
// 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 {
GlobalScope.launch {
for (notification in channelNotification1) {
player1Notifications.add(notification)
}
}
GlobalScope.launch {
for (notification in channelNotification2) {
player2Notifications.add(notification)
}
}
}
// Player 1 actions
val player1Job =
launch {
createGameWithCommandsInChannels(channelCommand1, gameId, user1) {
joinTheGame()
player1Notifications.waitNotification<WelcomeToTheGameNotification> {
players.map { it.userId }.contains(user1.id)
}
player1HasJoin = true
player1Notifications.waitNotification<PlayerAsJoinTheGameNotification> {
player.userId == user2.id
}
readyToPlay()
player1Notifications.waitNotification<PlayerWasReadyNotification> {
playerId == getPlayer(user2).id
}
playedCard1 =
player1Notifications
.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }
.hand
.first()
.apply {
this.shouldBeInstanceOf<Card.NumericCard>()
number shouldEqual 1
color shouldEqual Card.Color.Red
}
player1Notifications.waitNotification<ItsTheTurnOfNotification> {
if (player.userId == user2.id) error("WRONG PLAYER TURN")
player.userId == user1.id
}
game
.shouldBeInstanceOf<GameStarted>()
.discardPile
.topCard
.shouldNotBeNull()
.shouldBeInstanceOf<Card.NumericCard> {
it.number shouldEqual 0
it.color shouldEqual Card.Color.Red
}
playCard(playedCard1!!)
player1Notifications.waitNotification<ItsTheTurnOfNotification> {
player == getPlayer(user2)
}
player1Notifications.waitNotification<PlayerAsPlayACardNotification> {
playerId == getPlayer(user2).id && card == playedCard2
}
playedCard1 =
assertInstanceOf<GameStarted>(game)
.playableCards(currentPlayer.id)
.first()
playedCard1.run {
this.shouldBeInstanceOf<Card.NumericCard>()
number shouldEqual 2
color shouldEqual Card.Color.Red
}
playCard(playedCard1)
player1Notifications.waitNotification<ItsTheTurnOfNotification> {
player == getPlayer(user2)
}
}
}
// Player 2 actions
val player2Job =
launch {
createGameWithCommandsInChannels(channelCommand2, gameId, user2) {
// wait player 1 has joined the game
until(3.seconds) { player1HasJoin }
joinTheGame()
player2Notifications.waitNotification<WelcomeToTheGameNotification> {
players.map { it.userId }.contains(user1.id) &&
players.map { it.userId }.contains(user2.id)
}
player2Notifications.waitNotification<PlayerWasReadyNotification> { playerId == getPlayer(user1).id }
readyToPlay()
playedCard2 =
player2Notifications
.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }
.hand
.first()
.apply {
this.shouldBeInstanceOf<Card.NumericCard>()
number shouldEqual 8
color shouldEqual Card.Color.Red
}
player2Notifications.waitNotification<ItsTheTurnOfNotification> {
if (player.userId == user2.id) error("WRONG PLAYER TURN")
player.userId == user1.id
}
player2Notifications.waitNotification<PlayerAsPlayACardNotification> {
playerId == getPlayer(user1).id && card == playedCard1
}
player2Notifications.waitNotification<ItsTheTurnOfNotification> {
player == currentPlayer
}
game
.shouldBeInstanceOf<GameStarted>()
.discardPile
.topCard
.shouldNotBeNull()
.shouldBeInstanceOf<Card.NumericCard> {
it.number shouldEqual 1
it.color shouldEqual Card.Color.Red
}
playCard(playedCard2)
player2Notifications.waitNotification<ItsTheTurnOfNotification> {
player.userId == user1.id
}
player2Notifications.waitNotification<PlayerAsPlayACardNotification> {
playerId == currentPlayer.id && card == playedCard2
}
}
}
// Wait the end of the game
joinAll(player1Job, player2Job)
// Build the last state from the event store
val game = gameRepository.get(gameId)
assertInstanceOf<GameStarted>(game)
// Check if the state is correct
game.aggregateId shouldBeEqual gameId
game.players.map { it.userId } shouldContainExactly setOf(user1.id, user2.id)
assertNotNull(game.players.find { it.userId == user1.id })
.hand.size shouldBeEqual 5
assertNotNull(game.players.find { it.userId == user2.id })
.hand.size shouldBeEqual 6
game.direction shouldBeEqual Game.Direction.CLOCKWISE
assertNotNull(game.lastPlayer?.userId) shouldBeEqual user1.id
assertNotNull(game.discardPile.topCard) shouldBeEqual assertNotNull(playedCard1)
}
}
}
})
context(user: User)
private suspend inline fun <reified T : Notification> MutableList<Notification>.waitNotification(crossinline block: T.() -> Boolean): T {
println("NOTIFICATION WAITING: ${T::class.simpleName} for user: ${user.username}")
return eventually(3.seconds) {
filterIsInstance<T>()
.first { block(it) }
.also { remove(it) }
}.also { println("NOTIFICATION RECEIVED: ${T::class.simpleName} for user: ${user.username}") }
}
@@ -0,0 +1,121 @@
package eventDemo.contexts.game.application.eventStore
import ch.qos.logback.classic.Level
import com.rabbitmq.client.impl.ForgivingExceptionHandler
import eventDemo.Tag
import eventDemo.contexts.auth.application.eventStores.UserRepository
import eventDemo.contexts.game.application.eventStores.GameEventStoreRepository
import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.testHelpers.CreateGameWithCommandsHelpers
import eventDemo.testHelpers.CreateGameWithCommandsHelpers.joinTheGame
import eventDemo.testHelpers.createNewUser
import eventDemo.testHelpers.testKoinApplicationWithConfig
import eventDemo.testHelpers.withLogLevel
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.assertions.nondeterministic.eventuallyConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.equals.shouldBeEqual
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import org.slf4j.Logger
import java.util.UUID
import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds
@OptIn(DelicateCoroutinesApi::class)
class GameEventStoreRepositoryTest :
FunSpec({
tags(Tag.Postgresql)
val user1 = createNewUser("user1")
val user2 = createNewUser("user2")
test("GameRepository should build return the game after dispatch commands") {
testKoinApplicationWithConfig {
get<UserRepository>().run {
save(user1)
}
CreateGameWithCommandsHelpers.createGameWithCommands {
user1.joinTheGame()
getPlayer(user1).userId shouldBeEqual user1.id
}
}
}
test("get should build the last version of the state") {
withLogLevel(
ForgivingExceptionHandler::class.java.name to Level.OFF,
) {
testKoinApplicationWithConfig {
val repo = get<GameEventStoreRepository>()
get<UserRepository>().run {
save(user1)
save(user2)
}
CreateGameWithCommandsHelpers.createGameWithCommands {
user1.joinTheGame()
assertNotNull(repo.get(gameId)).run {
players.isNotEmpty() shouldBeEqual true
players.get(user1.id).userId shouldBeEqual user1.id
}
user2.joinTheGame()
assertNotNull(repo.get(gameId)).run {
players.isNotEmpty() shouldBeEqual true
players.size shouldBeEqual 2
players.get(user1.id).userId shouldBeEqual user1.id
players.get(user2.id).userId shouldBeEqual user2.id
}
}
}
}
}
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,
) {
var aggregateIds: MutableList<GameId> = mutableListOf()
testKoinApplicationWithConfig {
val repo = get<GameRepository>()
val gameName = "testGame${UUID.randomUUID()}"
(1..2)
.map { treadN ->
GlobalScope
.launch {
CreateGameWithCommandsHelpers.createGameWithCommands("testGame $treadN") {
repeat(3) { userN ->
val userX = createNewUser("userX $treadN:$userN")
get<UserRepository>().save(userX)
userX.joinTheGame()
}
aggregateIds.add(gameId)
}
}
}.joinAll()
eventually(
eventuallyConfig {
duration = 5.seconds
interval = 1.seconds
includeFirst = false
},
) {
aggregateIds shouldHaveSize 2
aggregateIds.forEach {
assertNotNull(repo.get(it)).run {
version shouldBeEqual 4
players shouldHaveSize 3
}
}
}
}
}
}
})
@@ -0,0 +1,85 @@
package eventDemo.contexts.game.application.notification
import eventDemo.contexts.auth.application.eventStores.UserEventStoreRepository
import eventDemo.contexts.auth.application.eventStores.UserRepository
import eventDemo.contexts.auth.infrastructure.persistence.eventStore.UserEventStoreInMemory
import eventDemo.contexts.game.application.command.handlers.GameCommandHandlerDispatcher
import eventDemo.contexts.game.application.command.handlers.JoinTheGameHandler
import eventDemo.contexts.game.application.command.handlers.PlayCardHandler
import eventDemo.contexts.game.application.command.handlers.ReadyToPlayHandler
import eventDemo.contexts.game.application.command.handlers.TakeCartFromDrawPileHandler
import eventDemo.contexts.game.application.command.models.JoinTheGameCommand
import eventDemo.contexts.game.application.eventStores.GameEventStoreRepository
import eventDemo.contexts.game.application.notification.models.Notification
import eventDemo.contexts.game.application.notification.models.WelcomeToTheGameNotification
import eventDemo.contexts.game.infrastructure.persistence.eventBus.GameEventBusInMemory
import eventDemo.contexts.game.infrastructure.persistence.eventStore.GameEventStoreInMemory
import eventDemo.sharedKernel.UserId
import eventDemo.testHelpers.createNewUser
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.equals.shouldEqual
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import org.junit.jupiter.api.assertInstanceOf
import kotlin.time.Duration.Companion.seconds
class EventToNotificationSubscriberTest :
FunSpec({
test("When event when send to the bus, a notification should be published") {
val bus = GameEventBusInMemory()
val gameRepository = GameEventStoreRepository(GameEventStoreInMemory())
val userRepository = UserEventStoreRepository(UserEventStoreInMemory())
val subscriber =
EventToNotificationSubscriber(
bus,
gameRepository,
)
val commentDispatcher =
GameCommandHandlerDispatcher(
PlayCardHandler(gameRepository, bus),
ReadyToPlayHandler(gameRepository, bus),
JoinTheGameHandler(gameRepository, bus, userRepository),
TakeCartFromDrawPileHandler(gameRepository, bus),
)
val notificationChannel = Channel<Notification>(Channel.BUFFERED)
val game = gameRepository.create()
val user1 = createNewUser("user1")
val user2 = createNewUser("user2")
userRepository.run {
save(user1)
save(user2)
}
val player1Notifications = mutableListOf<Notification>()
GlobalScope.launch {
for (notification in notificationChannel) {
player1Notifications.add(notification)
}
}
commentDispatcher.dispatch(JoinTheGameCommand(user1.id, JoinTheGameCommand.Payload(game.aggregateId)))
subscriber
.subscribeToEventsAndSendNotification(
game.aggregateId,
user2.id,
notificationChannel,
).use {
commentDispatcher.dispatch(JoinTheGameCommand(user2.id, JoinTheGameCommand.Payload(game.aggregateId)))
}
eventually(duration = 1.seconds) {
player1Notifications.size shouldEqual 1
}
player1Notifications.first().let { notification ->
assertInstanceOf<WelcomeToTheGameNotification>(notification)
notification.players.map { it.userId } shouldContain user2.id
}
}
})
@@ -0,0 +1,301 @@
package eventDemo.contexts.game.application.notification
import eventDemo.contexts.game.application.notification.models.ItsTheTurnOfNotification
import eventDemo.contexts.game.application.notification.models.PilesShuffledNotification
import eventDemo.contexts.game.application.notification.models.PlayerAsPlayACardNotification
import eventDemo.contexts.game.application.notification.models.PlayerHavePassNotification
import eventDemo.contexts.game.application.notification.models.PlayerWasReadyNotification
import eventDemo.contexts.game.application.notification.models.TheGameWasStartedNotification
import eventDemo.contexts.game.application.notification.models.WelcomeToTheGameNotification
import eventDemo.contexts.game.application.notification.models.YourNewCardNotification
import eventDemo.contexts.game.domain.events.CardIsPlayedEvent
import eventDemo.contexts.game.domain.events.DrawFilledWithDiscardEvent
import eventDemo.contexts.game.domain.events.GameStartedEvent
import eventDemo.contexts.game.domain.events.NewPlayerEvent
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
import eventDemo.contexts.game.domain.events.PlayerReadyEvent
import eventDemo.contexts.game.domain.game.Card
import eventDemo.contexts.game.domain.game.DiscardPile
import eventDemo.contexts.game.domain.game.DrawPile
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.domain.game.PlayerHand
import eventDemo.contexts.game.domain.game.PlayerList
import eventDemo.contexts.game.domain.game.gameState.GameCreated
import eventDemo.contexts.game.domain.game.gameState.GameStarted
import eventDemo.sharedKernel.UserId
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.assertInstanceOf
class ToNotificationTest :
FunSpec({
val player1 =
Player(
name = "Bob",
userId = UserId(),
hand = PlayerHand(setOf(Card.NumericCard(1, Card.Color.Red))),
id = Player.PlayerId(),
)
val player2 =
Player(
name = "John",
userId = UserId(),
hand = PlayerHand(setOf(Card.NumericCard(1, Card.Color.Red))),
id = Player.PlayerId(),
)
test("NewPlayerEvent") {
val game =
GameCreated(
aggregateId = GameId(),
version = 1,
players = PlayerList(setOf(player1)),
recordedEvents = setOf(),
)
NewPlayerEvent(
game.aggregateId,
version = 2,
player = player1,
).toNotification(
game = game,
currentUserId = player1.userId,
).let {
it.toList().size shouldBe 1
// Check if the user is
assertInstanceOf<WelcomeToTheGameNotification>(it.first()).run {
players.size shouldBe 1
players.first().name shouldBe "Bob"
}
}
}
test("PlayerReadyEvent") {
val game =
GameCreated(
aggregateId = GameId(),
version = 1,
players = PlayerList(setOf(player1)),
recordedEvents = setOf(),
)
PlayerReadyEvent(
game.aggregateId,
version = 2,
playerId = player1.id,
).toNotification(
game = game,
currentUserId = player1.userId,
).let {
it.toList().size shouldBe 1
assertInstanceOf<PlayerWasReadyNotification>(it.first()).let {
it.playerId shouldBe player1.id
}
}
}
test("PlayerHaveDrawCardEvent on current player") {
val game =
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
drawPile = DrawPile(),
discardPile = DiscardPile(),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
currentColor = Card.Color.Red,
version = 1,
recordedEvents = setOf(),
)
val card = Card.NumericCard(1, Card.Color.Blue)
PlayerHaveDrawCardEvent(
game.aggregateId,
version = 2,
playerId = player1.id,
takenCards = setOf(card),
).toNotification(
game = game,
currentUserId = player1.userId,
).let {
it.toList().size shouldBe 2
it.toList().let { notifications ->
assertInstanceOf<YourNewCardNotification>(notifications.first()).let {
it.cards.first() shouldBe card
}
assertInstanceOf<ItsTheTurnOfNotification>(notifications[1]).let {
it.player.id shouldBe player2.id
}
}
}
}
test("PlayerHaveDrawCardEvent on other player") {
val game =
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
drawPile = DrawPile(),
discardPile = DiscardPile(),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
currentColor = Card.Color.Red,
version = 1,
recordedEvents = setOf(),
)
val card = Card.NumericCard(1, Card.Color.Blue)
PlayerHaveDrawCardEvent(
game.aggregateId,
version = 2,
playerId = player1.id,
takenCards = setOf(card),
).toNotification(
game = game,
currentUserId = player2.userId,
).let {
it.toList().size shouldBe 2
it.toList().let { notifications ->
assertInstanceOf<PlayerHavePassNotification>(notifications.first()).let {
it.playerId shouldBe player1.id
}
assertInstanceOf<ItsTheTurnOfNotification>(notifications[1]).let {
it.player.id shouldBe player2.id
}
}
}
}
test("CardIsPlayedEvent on current player") {
val game =
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
drawPile = DrawPile(),
discardPile = DiscardPile(),
lastPlayerId = player2.id,
nextPlayerId = player1.id,
currentColor = Card.Color.Red,
version = 1,
recordedEvents = setOf(),
)
val card = Card.NumericCard(1, Card.Color.Blue)
CardIsPlayedEvent(
game.aggregateId,
version = 2,
playerId = player1.id,
card = card,
).toNotification(
game = game,
currentUserId = player1.userId,
).toList()
.let { notifications ->
notifications.size shouldBe 2
assertInstanceOf<PlayerAsPlayACardNotification>(notifications.first()).let {
it.playerId shouldBe player1.id
it.card shouldBe card
}
assertInstanceOf<ItsTheTurnOfNotification>(notifications[1]).let {
it.player.id shouldBe player1.id
}
}
}
test("CardIsPlayedEvent on other player") {
val game =
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
drawPile = DrawPile(),
discardPile = DiscardPile(),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
currentColor = Card.Color.Red,
version = 1,
recordedEvents = setOf(),
)
val card = Card.NumericCard(1, Card.Color.Blue)
CardIsPlayedEvent(
game.aggregateId,
version = 2,
playerId = player2.id,
card = card,
).toNotification(
game = game,
currentUserId = player1.userId,
).let {
it.toList().size shouldBe 2
it.toList().let { notifications ->
assertInstanceOf<PlayerAsPlayACardNotification>(notifications.first()).let {
it.playerId shouldBe player2.id
it.card shouldBe card
}
assertInstanceOf<ItsTheTurnOfNotification>(notifications[1]).let {
it.player.id shouldBe player2.id
}
}
}
}
test("DrawFilledWithDiscardEvent") {
val game =
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
drawPile = DrawPile(),
discardPile = DiscardPile(),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
currentColor = Card.Color.Red,
version = 1,
recordedEvents = setOf(),
)
DrawFilledWithDiscardEvent(
game.aggregateId,
version = 2,
newDrawPile = DrawPile(),
newDiscardPile = DiscardPile(),
).toNotification(
game = game,
currentUserId = player1.userId,
).let {
it.toList().size shouldBe 1
assertInstanceOf<PilesShuffledNotification>(it.first())
}
}
test("GameStartedEvent") {
val game =
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
drawPile = DrawPile(),
discardPile = DiscardPile(),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
currentColor = Card.Color.Red,
version = 1,
recordedEvents = setOf(),
)
GameStartedEvent(
game.aggregateId,
version = 2,
firstPlayer = player1.id,
playersHans =
mapOf(
player1.id to player1.hand,
player2.id to player2.hand,
),
drawPile = DrawPile(),
discardPile = DiscardPile(),
).toNotification(
game = game,
currentUserId = player1.userId,
).toList()
.let { notifications ->
notifications.size shouldBe 2
assertInstanceOf<TheGameWasStartedNotification>(notifications.first()).let {
it.hand.size shouldBe 1
it.hand.first() shouldBe player1.hand.cards.first()
}
assertInstanceOf<ItsTheTurnOfNotification>(notifications[1]).let {
it.player.id shouldBe player2.id
}
}
}
})
@@ -0,0 +1,46 @@
package eventDemo.contexts.game.domain.game
import eventDemo.testHelpers.act
import eventDemo.testHelpers.arrange
import eventDemo.testHelpers.assert
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.ints.shouldBeExactly
import org.junit.jupiter.api.assertInstanceOf
class PlayerHandTest :
FunSpec({
test("can be add new card to the hand") {
arrange {
PlayerHand(
setOf(
Card.NumericCard(0, Card.Color.Red),
Card.NumericCard(1, Card.Color.Red),
Card.NumericCard(2, Card.Color.Red),
),
)
}.act { hand ->
hand.withNewCards(setOf(Card.NumericCard(3, Card.Color.Red)))
}.assert { hand ->
hand.size shouldBeExactly 4
}
}
test("can be remove card to the hand") {
arrange {
PlayerHand(
setOf(
Card.NumericCard(0, Card.Color.Red),
Card.NumericCard(1, Card.Color.Red),
Card.NumericCard(2, Card.Color.Red),
),
)
}.act { hand ->
hand.withoutTheCards(setOf(hand.cards.elementAt(0)))
}.assert { hand ->
hand.size shouldBeExactly 2
assertInstanceOf<Set<Card.NumericCard>>(hand.cards)
hand.cards.elementAt(0).number shouldBeExactly 1
hand.cards.elementAt(1).number shouldBeExactly 2
}
}
})
@@ -0,0 +1,389 @@
package eventDemo.contexts.game.domain.game.gameState
import eventDemo.contexts.game.domain.events.CardIsPlayedEvent
import eventDemo.contexts.game.domain.events.DrawFilledWithDiscardEvent
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
import eventDemo.contexts.game.domain.events.PlayerWinEvent
import eventDemo.contexts.game.domain.game.Card
import eventDemo.contexts.game.domain.game.DiscardPile
import eventDemo.contexts.game.domain.game.DrawPile
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.domain.game.PlayerHand
import eventDemo.contexts.game.domain.game.PlayerList
import eventDemo.contexts.game.domain.game.gameState.Game.Direction
import eventDemo.sharedKernel.UserId
import eventDemo.testHelpers.act
import eventDemo.testHelpers.assert
import io.kotest.core.spec.style.FunSpec
import io.kotest.datatest.withData
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.assertInstanceOf
class GameStartedTest :
FunSpec({
context(GameStarted::canBePlayThisCard.name) {
val dataOk: Map<String, Triple<Card, Card, Card.Color?>> =
listOf(
Triple(
Card.NumericCard(0, Card.Color.Red),
Card.NumericCard(5, Card.Color.Red),
null,
),
Triple(
Card.NumericCard(0, Card.Color.Red),
Card.NumericCard(0, Card.Color.Blue),
null,
),
Triple(
Card.NumericCard(0, Card.Color.Green),
Card.NumericCard(0, Card.Color.Red),
null,
),
Triple(
Card.NumericCard(0, Card.Color.Red),
Card.Plus2Card(Card.Color.Red),
null,
),
Triple(
Card.NumericCard(0, Card.Color.Red),
Card.Plus2Card(Card.Color.Red),
null,
),
Triple(
Card.NumericCard(0, Card.Color.Red),
Card.Plus4Card(),
null,
),
Triple(
Card.Plus4Card(),
Card.Plus4Card(),
Card.Color.Red,
),
Triple(
Card.Plus4Card(),
Card.NumericCard(0, Card.Color.Blue),
Card.Color.Blue,
),
Triple(
Card.Plus2Card(Card.Color.Red),
Card.Plus2Card(Card.Color.Blue),
null,
),
Triple(
Card.Plus2Card(Card.Color.Red),
Card.Plus4Card(),
null,
),
Triple(
Card.Plus2Card(Card.Color.Red),
Card.ChangeColorCard(),
null,
),
Triple(
Card.Plus2Card(Card.Color.Red),
Card.NumericCard(0, Card.Color.Red),
null,
),
Triple(
Card.NumericCard(0, Card.Color.Red),
Card.ChangeColorCard(),
null,
),
Triple(
Card.ReverseCard(Card.Color.Red),
Card.NumericCard(0, Card.Color.Red),
null,
),
).associateBy { "I can play ${it.second} on ${it.first}${if (it.third != null) " when choose color is ${it.third}" else ""}" }
withData(dataOk) {
canBePlayThisCard(
it.first,
it.second,
it.third,
) shouldBe true
}
val dataKo: Map<String, Triple<Card, Card, Card.Color?>> =
listOf(
Triple(
Card.NumericCard(0, Card.Color.Red),
Card.NumericCard(9, Card.Color.Blue),
null,
),
Triple(
Card.NumericCard(0, Card.Color.Red),
Card.Plus2Card(Card.Color.Blue),
null,
),
Triple(
Card.NumericCard(0, Card.Color.Red),
Card.Plus2Card(Card.Color.Blue),
null,
),
Triple(
Card.Plus4Card(),
Card.NumericCard(0, Card.Color.Blue),
Card.Color.Red,
),
).associateBy { "I cannot play ${it.second} on ${it.first}${if (it.third != null) " when choose color is ${it.third}" else ""}" }
withData(dataKo) {
canBePlayThisCard(
it.first,
it.second,
it.third,
) shouldBe false
}
}
context("applyEvent") {
context("${CardIsPlayedEvent::class.simpleName}") {
test("with numeric card") {
val card1 = Card.NumericCard(2, Card.Color.Red)
assert {
gameWithCard(
played1Hand = PlayerHand(cards = setOf(card1)),
onTheDiscardPile = Card.NumericCard(0, Card.Color.Red),
).apply { nextPlayer shouldBe player1 }
}.act {
it.applyEvent(
CardIsPlayedEvent(
it.aggregateId,
card1,
playerId = it.player1.id,
version = it.version + 1,
),
)
}.assert {
it.currentColor shouldBe Card.Color.Red
assertInstanceOf<Card.NumericCard>(it.lastPlayedCard).number shouldBe 2
it.direction shouldBe Game.Direction.CLOCKWISE
it.nextPlayer shouldBe it.player2
it.player1.hand.size shouldBe 0
}
}
test("with revert turn card") {
val card1 = Card.ReverseCard(Card.Color.Red)
assert {
gameWithCard(
played1Hand = PlayerHand(cards = setOf(card1)),
onTheDiscardPile = Card.NumericCard(0, Card.Color.Red),
).apply { nextPlayer shouldBe player1 }
}.act {
it.applyEvent(
CardIsPlayedEvent(
it.aggregateId,
card1,
playerId = it.player1.id,
version = it.version + 1,
),
)
}.assert {
it.currentColor shouldBe Card.Color.Red
assertInstanceOf<Card.ReverseCard>(it.lastPlayedCard).color shouldBe Card.Color.Red
it.direction shouldBe Game.Direction.COUNTER_CLOCKWISE
it.nextPlayer shouldBe it.player3
it.player1.hand.size shouldBe 0
}
}
}
test("${DrawFilledWithDiscardEvent::class.simpleName}") {
val card1 = Card.NumericCard(1, Card.Color.Blue)
val card2 = Card.NumericCard(2, Card.Color.Yellow)
val card3 = Card.NumericCard(3, Card.Color.Red)
assert {
val player1 = Player("Player 1", UserId())
val player2 = Player("Player 2", UserId())
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1)),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
drawPile = DrawPile(),
discardPile =
DiscardPile(
setOf(
card1,
card2,
card3,
),
),
currentColor = card3.color,
version = 0,
recordedEvents = emptySet(),
)
}.act {
it.applyEvent(
DrawFilledWithDiscardEvent(
it.aggregateId,
version = it.version + 1,
newDrawPile =
DrawPile(
setOf(
card1,
card2,
),
),
newDiscardPile =
DiscardPile(
setOf(
card3,
),
),
),
)
}.assert {
it.currentColor shouldBe Card.Color.Red
assertInstanceOf<Card.NumericCard>(it.lastPlayedCard).color shouldBe Card.Color.Red
}
}
test("${PlayerHaveDrawCardEvent::class.simpleName}") {
val card1 = Card.NumericCard(1, Card.Color.Blue)
val card2 = Card.NumericCard(2, Card.Color.Red)
val card3 = Card.NumericCard(3, Card.Color.Red)
val card4 = Card.NumericCard(4, Card.Color.Red)
val player1 = Player("Jo", UserId())
val player2 = Player("Bob", UserId())
assert {
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1)),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
drawPile =
DrawPile(
setOf(
card2,
card3,
card4,
),
),
discardPile =
DiscardPile(
setOf(
card1,
),
),
currentColor = card1.color,
version = 0,
recordedEvents = emptySet(),
)
}.act {
it.applyEvent(
PlayerHaveDrawCardEvent(
it.aggregateId,
version = it.version + 1,
playerId = it.player1.id,
takenCards =
setOf(
card2,
card3,
),
),
)
}.assert {
it.player1.hand
.run {
cards.elementAt(0) shouldBe card2
cards.elementAt(1) shouldBe card3
}
}
}
test("${PlayerWinEvent::class.simpleName}") {
val player1 = Player("Jo", UserId())
val player2 = Player("Poo", UserId())
assert {
GameStarted(
aggregateId = GameId(),
players = PlayerList(setOf(player1, player2)),
lastPlayerId = player1.id,
nextPlayerId = player2.id,
drawPile = DrawPile(),
discardPile = DiscardPile(),
currentColor = Card.Color.Yellow,
version = 0,
recordedEvents = emptySet(),
)
}.act {
it.applyEvent(
PlayerWinEvent(
it.aggregateId,
version = it.version + 1,
playerId = it.player1.id,
),
)
}.assert {
it.playerWins.size shouldBe 1
it.playersInGame.size shouldBe 0
it.players.size shouldBe 2
}
}
}
test("nextPlayer") { }
test("players") { }
test("currentColor") { }
test("playedTurnHistory") { }
test("direction") { }
test("playerWins") { }
test("version") { }
test("recordedEvents") { }
})
private val GameStarted.player1: Player
get() = players.players.elementAt(0)
private val GameStarted.player2: Player
get() = players.players.elementAt(1)
private val GameStarted.player3: Player
get() = players.players.elementAt(2)
private fun canBePlayThisCard(
onTheDiscardPile: Card,
playedCard: Card,
chosenColor: Card.Color? = null,
): Boolean =
gameWithCard(
played1Hand = PlayerHand(setOf(playedCard)),
onTheDiscardPile = onTheDiscardPile,
chosenColor = chosenColor,
).run {
canBePlayThisCard(playedCard)
}
private fun gameWithCard(
played1Hand: PlayerHand,
played2Hand: PlayerHand = PlayerHand(setOf(Card.NumericCard(9, Card.Color.Yellow))),
onTheDiscardPile: Card,
chosenColor: Card.Color? = null,
): GameStarted {
val player1 = Player("Tesla", UserId(), hand = played1Hand)
val player2 = Player("Einstein", UserId(), hand = played2Hand)
val player3 = Player("Curie", UserId(), hand = PlayerHand(setOf(Card.NumericCard(8, Card.Color.Yellow))))
val players = PlayerList(setOf(player1, player2, player3))
return GameStarted(
aggregateId = GameId(),
players = players,
lastPlayerId = player3.id,
nextPlayerId = players.nextPlayerTurn(player3.id, Direction.CLOCKWISE),
discardPile = DiscardPile(setOf(onTheDiscardPile)),
drawPile = DrawPile(),
currentColor = (onTheDiscardPile as? Card.CardWithColor)?.color ?: chosenColor ?: error("no color"),
version = 0,
recordedEvents = emptySet(),
)
}
@@ -0,0 +1,41 @@
package eventDemo.contexts.game.domain.game.gameState
import eventDemo.contexts.game.domain.game.Card
import io.kotest.assertions.retry
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import kotlin.time.Duration.Companion.seconds
class NewDeckTest :
FunSpec({
test("newDeck") {
newDeck().let {
it shouldNotBe null
it.filterIsInstance<Card.NumericCard>() shouldHaveSize 76
it.filterIsInstance<Card.Plus2Card>() shouldHaveSize 8
it.filterIsInstance<Card.ReverseCard>() shouldHaveSize 8
it.filterIsInstance<Card.PassCard>() shouldHaveSize 8
it.filterIsInstance<Card.Plus4Card>() shouldHaveSize 4
it.filterIsInstance<Card.ChangeColorCard>() shouldHaveSize 4
it shouldHaveSize 108
}
}
test("shuffleDeck") {
val deck = (0..9).map { Card.NumericCard(it, Card.Color.Red) }
deck.run {
this[3].number shouldBe 3
}
should {
retry(maxRetry = 4, timeout = 1.seconds) {
deck.shuffled().run {
this[3].number shouldNotBe 3
}
}
}
}
})
@@ -0,0 +1,10 @@
package eventDemo.contexts.game.intrastructure
import eventDemo.contexts.auth.domain.User
import eventDemo.contexts.auth.infrastructure.configure.makeJwt
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.header
internal fun HttpRequestBuilder.withAuth(user: User) {
header("Authorization", "Bearer ${user.makeJwt("secret")}")
}
@@ -1,5 +1,6 @@
package eventDemo.adapter.presenter.query
import eventDemo.configuration.ktor.defaultJsonSerializer
package eventDemo.contexts.game.intrastructure
import eventDemo.contexts.game.infrastructure.configuration.ktor.defaultJsonSerializer
import io.ktor.client.HttpClient
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json
@@ -1,7 +1,7 @@
package eventDemo.externalServices
package eventDemo.contexts.game.intrastructure.persistence.connectors
import eventDemo.Tag
import eventDemo.testKoinApplicationWithConfig
import eventDemo.testHelpers.testKoinApplicationWithConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
import javax.sql.DataSource
@@ -10,10 +10,9 @@ class PostgresqlTest :
FunSpec({
tags(Tag.Postgresql)
test("test connection with postgresql") {
test("test connection with PostgreSQL") {
testKoinApplicationWithConfig {
val datasource by inject<DataSource>()
datasource.connection.use { connection ->
get<DataSource>().connection.use { connection ->
connection
.prepareStatement(
"""
@@ -0,0 +1,60 @@
package eventDemo.contexts.game.intrastructure.persistence.connectors
import com.rabbitmq.client.AMQP.BasicProperties
import com.rabbitmq.client.BuiltinExchangeType
import com.rabbitmq.client.ConnectionFactory
import com.rabbitmq.client.DefaultConsumer
import com.rabbitmq.client.Envelope
import eventDemo.Tag
import eventDemo.testHelpers.spyPing
import eventDemo.testHelpers.testKoinApplicationWithConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.string.shouldStartWith
import java.util.UUID
import kotlin.time.Duration.Companion.seconds
class RabbitMQTest :
FunSpec({
tags(Tag.RabbitMQ)
test("test connection with RabbitMQ") {
testKoinApplicationWithConfig {
val exchangeName = "test_" + UUID.randomUUID()
get<ConnectionFactory>().newConnection().use { connection ->
connection
.createChannel()
.use { channel ->
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT)
val queue = channel.queueDeclare("myQueue", true, false, false, emptyMap()).queue
channel.queueBind(queue, exchangeName, "")
spyPing(3.seconds, exactly = 2) { ping ->
channel
.basicConsume(
queue,
object : DefaultConsumer(channel) {
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: BasicProperties,
body: ByteArray,
) {
val msg = body.toString(Charsets.UTF_8)
msg shouldStartWith "testMessage"
ping()
channel.basicAck(envelope.deliveryTag, false)
}
},
)
channel.basicPublish(exchangeName, "", BasicProperties(), "testMessage1".toByteArray())
channel.basicPublish(exchangeName, "", BasicProperties(), "testMessage2".toByteArray())
}
channel.queueDelete(queue)
channel.exchangeDelete(exchangeName)
}
}
}
}
})
@@ -0,0 +1,21 @@
package eventDemo.contexts.game.intrastructure.persistence.connectors
import eventDemo.Tag
import eventDemo.testHelpers.testKoinApplicationWithConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
import redis.clients.jedis.UnifiedJedis
class RedisTest :
FunSpec({
tags(Tag.Redis)
test("test connection with jedis") {
testKoinApplicationWithConfig {
get<UnifiedJedis>().also {
it.set("test", "test")
it.get("test") shouldBeEqual "test"
}
}
}
})
@@ -0,0 +1,44 @@
package eventDemo.contexts.game.intrastructure.persistence.eventBus
import com.rabbitmq.client.ConnectionFactory
import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.domain.events.NewPlayerEvent
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.infrastructure.persistence.eventBus.GameEventBusInMemory
import eventDemo.contexts.game.infrastructure.persistence.eventBus.GameEventBusInRabbinMQ
import eventDemo.sharedKernel.UserId
import eventDemo.testHelpers.spyPing
import eventDemo.testHelpers.testKoinApplicationWithConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.datatest.withData
import io.kotest.matchers.equals.shouldBeEqual
import kotlin.time.Duration.Companion.seconds
class GameEventBusInRabbitMQTest :
FunSpec({
context("Pub/sub") {
testKoinApplicationWithConfig {
val busListToTest: Map<String, GameEventBus> =
mapOf(
GameEventBusInMemory::class.java.simpleName to GameEventBusInMemory(),
GameEventBusInRabbinMQ::class.java.simpleName to GameEventBusInRabbinMQ(get<ConnectionFactory>()),
)
withData(busListToTest) { bus ->
spyPing(1.seconds, exactly = 2) { ping ->
val aggregateId = GameId()
val player1 = Player(name = "Tesla", UserId())
val player2 = Player(name = "Einstein", UserId())
bus.subscribe { obj ->
ping()
obj.aggregateId shouldBeEqual aggregateId
}
bus.publish(NewPlayerEvent(aggregateId, player1, 1))
bus.publish(NewPlayerEvent(aggregateId, player2, 2))
}
}
}
}
})
@@ -0,0 +1,128 @@
package eventDemo.contexts.game.intrastructure.rest
import eventDemo.contexts.auth.application.eventStores.UserRepository
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameList
import eventDemo.contexts.game.intrastructure.httpClient
import eventDemo.contexts.game.intrastructure.withAuth
import eventDemo.testHelpers.CreateGameWithCommandsHelpers
import eventDemo.testHelpers.CreateGameWithCommandsHelpers.joinTheGame
import eventDemo.testHelpers.CreateGameWithCommandsHelpers.readyToPlay
import eventDemo.testHelpers.createNewUser
import eventDemo.testHelpers.testApplicationWithConfig
import io.github.oshai.kotlinlogging.KotlinLogging
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.assertions.nondeterministic.eventuallyConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.equals.shouldBeEqual
import io.ktor.client.call.body
import io.ktor.client.request.accept
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
val logger = KotlinLogging.logger {}
class GameListRouteTest :
FunSpec({
test("/games with no game started") {
val user1 = createNewUser("user1")
testApplicationWithConfig({
get<UserRepository>().save(user1)
}) {
logger.info { "Starting player1" }
httpClient()
.get("/games") {
withAuth(user1)
accept(ContentType.Application.Json)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
val list = call.body<List<GameList>>()
assertTrue(list.isEmpty())
}
}
}
test("/games return a game with status OPENING") {
val user1 = createNewUser("user1")
testApplicationWithConfig({
get<UserRepository>().save(user1)
CreateGameWithCommandsHelpers.createGameWithCommands {
user1.joinTheGame()
}
}) {
// Wait until the projection is created
eventually(
eventuallyConfig {
initialDelay = 1.seconds
interval = 1.seconds
duration = 3.seconds
},
) {
httpClient()
.get("/games") {
withAuth(user1)
accept(ContentType.Application.Json)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
call.body<List<GameList>>().let {
assertNotNull(it)
assertTrue { it.isNotEmpty() }
it.firstOrNull()?.run {
status shouldBeEqual GameList.Status.OPENING
players shouldHaveSize 1
players.map { it.userId } shouldContain user1.id
winners shouldHaveSize 0
}
}
}
}
}
}
test("/games return a game with status IS_STARTED") {
val user1 = createNewUser("user1")
val user2 = createNewUser("user2")
testApplicationWithConfig({
CreateGameWithCommandsHelpers.createGameWithCommands {
get<UserRepository>().run {
save(user1)
save(user2)
}
user1.joinTheGame()
user2.joinTheGame()
getPlayer(user1).readyToPlay()
getPlayer(user2).readyToPlay()
}
}) {
eventually(
eventuallyConfig {
initialDelay = 1.seconds
interval = 1.seconds
duration = 3.seconds
},
) {
httpClient()
.get("/games") {
withAuth(user1)
accept(ContentType.Application.Json)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
call.body<List<GameList>>().first().let {
it.status shouldBeEqual GameList.Status.IS_STARTED
it.players shouldHaveSize 2
it.players.map { it.userId } shouldContain user1.id
it.players.map { it.userId } shouldContain user2.id
it.winners shouldHaveSize 0
}
}
}
}
}
})
@@ -1,65 +0,0 @@
package eventDemo.domain.command
import eventDemo.Tag
import eventDemo.domain.command.command.GameCommand
import eventDemo.domain.command.command.IWantToJoinTheGameCommand
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.event.projection.projectionListener.PlayerNotificationListener
import eventDemo.domain.notification.CommandSuccessNotification
import eventDemo.domain.notification.Notification
import eventDemo.domain.notification.WelcomeToTheGameNotification
import eventDemo.testKoinApplicationWithConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.equals.shouldBeEqual
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlin.test.assertIs
import kotlin.time.Duration.Companion.seconds
@OptIn(DelicateCoroutinesApi::class)
class GameCommandHandlerTest :
FunSpec({
tags(Tag.Postgresql)
test("handle a command should execute the command") {
withTimeout(5.seconds) {
testKoinApplicationWithConfig {
val commandHandler = get<GameCommandHandler>()
val notificationListener = get<PlayerNotificationListener>()
val gameId = GameId()
val player = Player("Tesla")
val channelCommand = Channel<GameCommand>(Channel.BUFFERED)
val channelNotification = Channel<Notification>(Channel.BUFFERED)
notificationListener.startListening(
player,
gameId,
) { channelNotification.trySendBlocking(it) }
GlobalScope.launch {
commandHandler.handleIncomingPlayerCommands(
player,
gameId,
channelCommand,
channelNotification,
)
}
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player)).also { sendCommand ->
channelCommand.send(sendCommand)
channelNotification.receive().let {
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
}
}
assertIs<WelcomeToTheGameNotification>(channelNotification.receive()).let {
it.players shouldContain player
}
}
}
}
})
@@ -1,8 +0,0 @@
package eventDemo.domain.command
import io.kotest.core.spec.style.FunSpec
class GameCommandRunnerTest :
FunSpec({
test("run should run the correct command") { }
})
@@ -1,9 +0,0 @@
package eventDemo.domain.command.command
import io.kotest.core.spec.style.FunSpec
class ICantPlayCommandTest :
FunSpec({
xtest("run should publish the event") { }
})
@@ -1,9 +0,0 @@
package eventDemo.domain.command.command
import io.kotest.core.spec.style.FunSpec
class IWantToJoinTheGameCommandTest :
FunSpec({
xtest("run should publish the event") { }
})
@@ -1,9 +0,0 @@
package eventDemo.domain.command.command
import io.kotest.core.spec.style.FunSpec
class IWantToPlayCardCommandTest :
FunSpec({
xtest("run should publish the event") { }
})
@@ -1,9 +0,0 @@
package eventDemo.domain.command.command
import io.kotest.core.spec.style.FunSpec
class IamReadyToPlayCommandTest :
FunSpec({
xtest("run should publish the event") { }
})
@@ -1,104 +0,0 @@
package eventDemo.domain.entity
import eventDemo.allCardCount
import eventDemo.allCards
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldBeUnique
import io.kotest.matchers.ints.shouldBeExactly
import kotlin.test.assertNotNull
class DeckTest :
FunSpec({
val totalCardsNumber = 104
test("newWithoutPlayers") {
// When
val deck = Deck.newWithoutPlayers()
// Then
deck.stack.size shouldBeExactly totalCardsNumber
deck.discard.size shouldBeExactly 0
deck.playersHands.size shouldBeExactly 0
deck.allCardCount() shouldBeExactly totalCardsNumber
deck.allCards().shouldBeUnique()
deck.allCards().map { it.id }.shouldBeUnique()
}
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()
val deck = Deck.newWithoutPlayers()
// When
val initDeck = deck.initHands(players)
// Then
initDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7)
initDeck.discard.size shouldBeExactly 0
initDeck.playersHands.size shouldBeExactly playerNumbers
initDeck.playersHands.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
initDeck.allCardCount() shouldBeExactly totalCardsNumber
}
test("takeOneCardFromStackTo player") {
// Given
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.takeOneCardFromStackTo(firstPlayer)
// Then
modifiedDeck.discard.size shouldBeExactly 0
modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) - 1
modifiedDeck.playersHands.size shouldBeExactly playerNumbers
assertNotNull(modifiedDeck.playersHands.getHand(firstPlayer)).size shouldBeExactly 7 + 1
modifiedDeck.playersHands
.filterKeys { it != firstPlayer.id }
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
}
test("putOneCardFromHand") {
// Given
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 card = deck.playersHands.getHand(firstPlayer)!!.first()
val modifiedDeck = deck.putOneCardFromHand(firstPlayer, card)
// Then
modifiedDeck.discard.size shouldBeExactly 1
modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7)
modifiedDeck.playersHands.size shouldBeExactly playerNumbers
assertNotNull(modifiedDeck.playersHands.getHand(firstPlayer)).size shouldBeExactly 6
modifiedDeck.playersHands
.filterKeys { it != firstPlayer.id }
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
}
test("placeFirstCardOnDiscard") {
// Given
val playerNumbers = 4
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
val deck = Deck.newWithoutPlayers().initHands(players)
// When
val modifiedDeck = deck.placeFirstCardOnDiscard()
// Then
modifiedDeck.discard.size shouldBeExactly 1
modifiedDeck.stack.size shouldBeExactly totalCardsNumber - (playerNumbers * 7) - 1
modifiedDeck.playersHands.size shouldBeExactly playerNumbers
modifiedDeck.playersHands
.forEach { (_, cards) -> cards.size shouldBeExactly 7 }
modifiedDeck.allCardCount() shouldBeExactly totalCardsNumber
}
})
@@ -1,41 +0,0 @@
package eventDemo.domain.entity
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.ints.shouldBeExactly
import kotlin.test.assertNotNull
class PlayerHandKtTest :
FunSpec({
test("addCards") {
// Given
val playerNumbers = 4
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
val firstPlayer = players.first()
val playersHands = PlayersHands(players)
val card = Card.NumericCard(0, Card.Color.Red)
// When
val newHands: PlayersHands = playersHands.addCards(firstPlayer, listOf(card))
assertNotNull(newHands.getHand(firstPlayer)).size shouldBeExactly 1
assertNotNull(newHands.getHand(players.last())).size shouldBeExactly 0
}
test("removeCard") {
// Given
val playerNumbers = 4
val players = (1..playerNumbers).map { Player(name = "name $it") }.toSet()
val firstPlayer = players.first()
val card1 = Card.NumericCard(1, Card.Color.Red)
val card2 = Card.NumericCard(2, Card.Color.Red)
val playersHands: PlayersHands =
PlayersHands(players)
.addCards(firstPlayer, listOf(card1, card2))
// When
val newHands: PlayersHands = playersHands.removeCard(firstPlayer, card1)
assertNotNull(newHands.getHand(firstPlayer)).size shouldBeExactly 1
assertNotNull(newHands.getHand(players.last())).size shouldBeExactly 0
}
})
@@ -1,15 +0,0 @@
package eventDemo.domain.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") { }
})
@@ -1,110 +0,0 @@
package eventDemo.domain.event
import eventDemo.adapter.infrastructure.event.GameEventBusInMemory
import eventDemo.adapter.infrastructure.event.GameEventStoreInMemory
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.event.event.GameEvent
import eventDemo.domain.event.event.NewPlayerEvent
import eventDemo.libs.event.VersionBuilderLocal
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.equals.shouldBeEqual
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlin.test.assertIs
import kotlin.test.assertNotNull
class GameEventHandlerTest :
FunSpec({
test("handle event should publish the event to the stream") {
// Given
val eventBus: GameEventBus = GameEventBusInMemory()
val eventStore: GameEventStore = spyk(GameEventStoreInMemory())
val handler =
GameEventHandler(
eventBus = eventBus,
eventStore = eventStore,
versionBuilder = VersionBuilderLocal(),
)
val gameId = GameId()
val player1 = Player("Tesla")
// When
handler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
// Then
coVerify(exactly = 1) { eventStore.publish(any()) }
eventStore.getStream(gameId).readAll().let { events ->
events shouldHaveSize 1
events.first().let {
assertIs<NewPlayerEvent>(it)
it.aggregateId shouldBeEqual gameId
it.player.name shouldBeEqual "Tesla"
}
}
}
test("handle event should publish the event to the bus") {
// Given
val eventBus: GameEventBus = spyk(GameEventBusInMemory())
val eventStore: GameEventStore = GameEventStoreInMemory()
val handler =
GameEventHandler(
eventBus = eventBus,
eventStore = eventStore,
versionBuilder = VersionBuilderLocal(),
)
val gameId = GameId()
val player1 = Player("Tesla")
// When
var event: GameEvent? = null
val spy = spyk<() -> Unit>()
eventBus.subscribe {
spy()
event = it
}
handler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
// Then
verify(exactly = 1) { spy() }
coVerify(exactly = 1) { eventBus.publish(any()) }
assertNotNull(event).let {
assertIs<NewPlayerEvent>(it)
it.aggregateId shouldBeEqual gameId
it.player.name shouldBeEqual "Tesla"
}
}
test("handle event should call version builder once") {
// Given
val eventBus: GameEventBus = GameEventBusInMemory()
val eventStore: GameEventStore = GameEventStoreInMemory()
val versionBuilder = spyk(VersionBuilderLocal())
val handler =
GameEventHandler(
eventBus = eventBus,
eventStore = eventStore,
versionBuilder = versionBuilder,
)
val gameId = GameId()
val player1 = Player("Tesla")
// When
handler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
// Then
verify(exactly = 1) { versionBuilder.buildNextVersion(any()) }
eventStore
.getStream(gameId)
.readAll()
.first()
.version shouldBeEqual 1
}
})
@@ -1,126 +0,0 @@
package eventDemo.domain.event.projection
import eventDemo.domain.entity.Card
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.event.event.CardIsPlayedEvent
import eventDemo.domain.event.event.GameStartedEvent
import eventDemo.domain.event.event.NewPlayerEvent
import eventDemo.domain.event.event.PlayerReadyEvent
import eventDemo.domain.event.event.disableShuffleDeck
import eventDemo.libs.event.VersionBuilderLocal
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
import kotlin.test.assertIs
import kotlin.test.assertNotNull
class GameStateBuilderTest :
FunSpec({
test("apply") {
disableShuffleDeck()
val versionBuilder = VersionBuilderLocal()
val gameId = GameId()
val player1 = Player(name = "Nikola")
val player2 = Player(name = "Einstein")
GameState(gameId)
.run {
val event =
NewPlayerEvent(
aggregateId = gameId,
player = player1,
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
state.isReady shouldBeEqual false
state.isStarted shouldBeEqual false
}
}.run {
val event =
NewPlayerEvent(
aggregateId = gameId,
player = player2,
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
state.players shouldBeEqual setOf(player1, player2)
}
}.run {
val event =
PlayerReadyEvent(
aggregateId = gameId,
player = player1,
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
state.readyPlayers shouldBeEqual setOf(player1)
}
}.run {
val event =
PlayerReadyEvent(
aggregateId = gameId,
player = player2,
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
state.readyPlayers shouldBeEqual setOf(player1, player2)
state.isReady shouldBeEqual true
state.isStarted shouldBeEqual false
}
}.run {
val event =
GameStartedEvent.new(
id = gameId,
players = setOf(player1, player2),
shuffleIsDisabled = true,
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
state.isStarted shouldBeEqual true
assertIs<Card.NumericCard>(state.deck.stack.first()).let {
it.number shouldBeEqual 6
it.color shouldBeEqual Card.Color.Red
}
}
}.run {
val playedCard = playableCards(player1)[0]
val event =
CardIsPlayedEvent(
aggregateId = gameId,
card = playedCard,
player = player1,
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
assertNotNull(state.cardOnCurrentStack) shouldBeEqual playedCard
assertIs<Card.NumericCard>(playedCard).let {
it.number shouldBeEqual 0
it.color shouldBeEqual Card.Color.Red
}
}
}.run {
val playedCard = playableCards(player2)[0]
val event =
CardIsPlayedEvent(
aggregateId = gameId,
card = playedCard,
player = player2,
version = versionBuilder.buildNextVersion(gameId),
)
apply(event).also { state ->
state.aggregateId shouldBeEqual gameId
assertNotNull(state.cardOnCurrentStack) shouldBeEqual playedCard
assertIs<Card.NumericCard>(playedCard).let {
it.number shouldBeEqual 7
it.color shouldBeEqual Card.Color.Red
}
}
}
}
})
@@ -1,136 +0,0 @@
package eventDemo.domain.event.projection
import ch.qos.logback.classic.Level
import com.rabbitmq.client.impl.ForgivingExceptionHandler
import eventDemo.Tag
import eventDemo.domain.command.GameCommandHandler
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.event.GameEventHandler
import eventDemo.domain.event.event.NewPlayerEvent
import eventDemo.testKoinApplicationWithConfig
import eventDemo.withLogLevel
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.assertions.nondeterministic.eventuallyConfig
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 kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import org.slf4j.Logger
import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds
@OptIn(DelicateCoroutinesApi::class, KotestInternal::class)
class GameStateRepositoryTest :
FunSpec({
tags(Tag.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<GameStateRepository>()
val eventHandler = get<GameEventHandler>()
eventHandler
.handle(aggregateId) { NewPlayerEvent(aggregateId = aggregateId, player = player1, version = it) }
.also {
// Wait until the projection is created
eventually(1.seconds) {
assertNotNull(repo.get(aggregateId)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1)
}
}
}
}
}
test("get should build the last version of the state") {
withLogLevel(
GameCommandHandler::class.java.name to Level.ERROR,
ForgivingExceptionHandler::class.java.name to Level.OFF,
) {
val aggregateId = GameId()
testKoinApplicationWithConfig {
val repo = get<GameStateRepository>()
val eventHandler = get<GameEventHandler>()
val projectionBus = get<GameProjectionBus>()
var state: GameState? = null
projectionBus.subscribe {
repo
.get(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.get(aggregateId)).also {
assertNotNull(it.players) shouldBeEqual setOf(player1, player2)
}
}
}
}
}
}
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,
) {
val aggregateId = GameId()
testKoinApplicationWithConfig {
val repo = get<GameStateRepository>()
val eventHandler = get<GameEventHandler>()
(1..10)
.map { r ->
GlobalScope
.launch {
repeat(20) { r2 ->
val playerX = Player("player$r$r2")
eventHandler
.handle(aggregateId) {
NewPlayerEvent(
aggregateId = aggregateId,
player = playerX,
version = it,
)
}
}
}
}.joinAll()
eventually(
eventuallyConfig {
duration = 60.seconds
interval = 2.seconds
includeFirst = false
},
) {
repo.get(aggregateId).run {
lastEventVersion shouldBeEqual 200
players shouldHaveSize 200
}
}
}
}
}
})
@@ -1,158 +0,0 @@
package eventDemo.domain.event.projection
import eventDemo.domain.entity.Card
import eventDemo.domain.entity.Deck
import eventDemo.domain.entity.Discard
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.entity.PlayersHands
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
class GameStateTest :
FunSpec({
val player1 = Player("Tesla")
val player2 = Player("Einstein")
test("isReady return false when all players in the game is not ready") {
GameState(
aggregateId = GameId(),
players = setOf(player1, player2),
readyPlayers = setOf(player1),
).isReady shouldBeEqual false
}
test("isReady return true when all players in the game is ready") {
GameState(
aggregateId = GameId(),
players = setOf(player1, player2),
readyPlayers = setOf(player1, player2),
).isReady shouldBeEqual true
}
xtest("nextPlayerTurn") { }
xtest("playerDiffIndex") { }
xtest("cardOnBoardIsForYou") { }
xtest("playableCards") { }
xtest("playerHasNoCardLeft") { }
test("canBePlayThisCard return true when a card can be played on the current game") {
canBePlayThisCard(
onTheDeck = Card.NumericCard(0, Card.Color.Red),
playedCard = Card.NumericCard(5, Card.Color.Red),
) shouldBeEqual true
canBePlayThisCard(
onTheDeck = Card.NumericCard(0, Card.Color.Red),
playedCard = Card.NumericCard(0, Card.Color.Blue),
) shouldBeEqual true
canBePlayThisCard(
onTheDeck = Card.NumericCard(0, Card.Color.Red),
playedCard = Card.NumericCard(0, Card.Color.Green),
) shouldBeEqual true
canBePlayThisCard(
onTheDeck = Card.NumericCard(0, Card.Color.Red),
playedCard = Card.Plus2Card(Card.Color.Red),
) shouldBeEqual true
canBePlayThisCard(
onTheDeck = Card.NumericCard(0, Card.Color.Red),
playedCard = Card.Plus2Card(Card.Color.Red),
) shouldBeEqual true
canBePlayThisCard(
onTheDeck = Card.NumericCard(0, Card.Color.Red),
playedCard = Card.Plus4Card(),
) shouldBeEqual true
canBePlayThisCard(
onTheDeck = Card.Plus4Card(),
playedCard = Card.Plus4Card(),
) shouldBeEqual true
canBePlayThisCard(
onTheDeck = Card.Plus4Card(),
playedCard = Card.Plus4Card(),
) shouldBeEqual true
canBePlayThisCard(
onTheDeck = Card.Plus2Card(Card.Color.Red),
playedCard = Card.Plus2Card(Card.Color.Blue),
) shouldBeEqual true
canBePlayThisCard(
onTheDeck = Card.NumericCard(0, Card.Color.Red),
playedCard = Card.ChangeColorCard(),
) shouldBeEqual true
canBePlayThisCard(
onTheDeck = Card.ReverseCard(Card.Color.Red),
playedCard = Card.NumericCard(0, Card.Color.Red),
) shouldBeEqual true
}
test("canBePlayThisCard return false when a card cannot be played on the current game") {
canBePlayThisCard(
onTheDeck = Card.NumericCard(0, Card.Color.Red),
playedCard = Card.NumericCard(9, Card.Color.Blue),
) shouldBeEqual false
canBePlayThisCard(
onTheDeck = Card.NumericCard(0, Card.Color.Red),
playedCard = Card.Plus2Card(Card.Color.Blue),
) shouldBeEqual false
canBePlayThisCard(
onTheDeck = Card.NumericCard(0, Card.Color.Red),
playedCard = Card.Plus2Card(Card.Color.Blue),
) shouldBeEqual false
canBePlayThisCard(
onTheDeck = Card.Plus2Card(Card.Color.Red),
playedCard = Card.Plus4Card(),
) shouldBeEqual false
canBePlayThisCard(
onTheDeck = Card.Plus2Card(Card.Color.Red),
playedCard = Card.ChangeColorCard(),
) shouldBeEqual false
canBePlayThisCard(
onTheDeck = Card.Plus2Card(Card.Color.Red),
playedCard = Card.NumericCard(0, Card.Color.Red),
) shouldBeEqual false
}
})
private fun canBePlayThisCard(
onTheDeck: Card,
playedCard: Card,
): Boolean {
val player1 = Player("Tesla")
return gameStateWithCard(
player = player1,
onTheDeck = onTheDeck,
playerHand = listOf(playedCard),
).canBePlayThisCard(player1, playedCard)
}
private fun gameStateWithCard(
player: Player,
onTheDeck: Card,
playerHand: List<Card>,
): GameState {
val player2 = Player("Einstein")
return GameState(
aggregateId = GameId(),
players = setOf(player, player2),
readyPlayers = setOf(player, player2),
lastCardPlayer = player2,
deck =
Deck(
discard = Discard(setOf(onTheDeck)),
playersHands = PlayersHands(mapOf(player.id to playerHand)),
),
)
}
@@ -1,37 +0,0 @@
package eventDemo.domain.event.projection.projectionListener
import eventDemo.adapter.infrastructure.event.projection.GameProjectionBusInMemory
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.event.event.NewPlayerEvent
import eventDemo.domain.event.projection.GameState
import eventDemo.domain.notification.WelcomeToTheGameNotification
import io.kotest.core.spec.style.FunSpec
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlin.test.assertIs
class PlayerNotificationListenerTest :
FunSpec({
test("startListening should react when a projection is sent to the bus") {
val player = Player("Tesla")
val gameId = GameId()
val bus = GameProjectionBusInMemory()
val state =
GameState(
aggregateId = gameId,
lastEvent = NewPlayerEvent(gameId, player, 1),
players = setOf(player),
)
val spy = spyk<() -> Unit>()
PlayerNotificationListener(bus).startListening(player, gameId) {
assertIs<WelcomeToTheGameNotification>(it)
spy()
}
bus.publish(state)
verify(exactly = 1) { spy() }
}
})
@@ -1,70 +0,0 @@
package eventDemo.externalServices
import com.rabbitmq.client.AMQP.BasicProperties
import com.rabbitmq.client.BuiltinExchangeType
import com.rabbitmq.client.ConnectionFactory
import com.rabbitmq.client.DefaultConsumer
import com.rabbitmq.client.Envelope
import eventDemo.Tag
import eventDemo.testKoinApplicationWithConfig
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.string.shouldStartWith
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import java.util.UUID
import kotlin.time.Duration.Companion.seconds
class RabbitMQTest :
FunSpec({
tags(Tag.RabbitMQ)
test("test connection with RabbitMQ") {
testKoinApplicationWithConfig {
val factory = get<ConnectionFactory>()
val exchangeName = "test_" + UUID.randomUUID()
val spy = spyk<() -> Unit>()
factory.newConnection().use { connection ->
connection
.createChannel()
.use { channel ->
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT)
val queue = channel.queueDeclare("qqq", true, false, false, emptyMap()).queue
channel.queueBind(queue, exchangeName, "")
channel
.basicConsume(
queue,
object : DefaultConsumer(channel) {
override fun handleDelivery(
consumerTag: String,
envelope: Envelope,
properties: BasicProperties,
body: ByteArray,
) {
val msg = body.toString(Charsets.UTF_8)
msg shouldStartWith "testMessage"
spy()
channel.basicAck(envelope.deliveryTag, false)
}
},
)
channel.basicPublish(exchangeName, "", BasicProperties(), "testMessage1".toByteArray())
channel.basicPublish(exchangeName, "", BasicProperties(), "testMessage2".toByteArray())
eventually(3.seconds) {
verify(exactly = 2) { spy() }
}
channel.queueDelete(queue)
channel.exchangeDelete(exchangeName)
}
}
}
}
})
@@ -1,20 +0,0 @@
package eventDemo.externalServices
import eventDemo.Tag
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
import redis.clients.jedis.JedisPooled
private val redisUrl = "redis://localhost:6379"
class RedisTest :
FunSpec({
tags(Tag.Redis)
test("test connection with jedis") {
JedisPooled(redisUrl).also {
it.set("test", "test")
it.get("test") shouldBeEqual "test"
}
}
})
+8 -15
View File
@@ -1,13 +1,10 @@
package eventDemo.libs.bus
import com.rabbitmq.client.ConnectionFactory
import io.kotest.assertions.nondeterministic.eventually
import eventDemo.testHelpers.spyPing
import io.kotest.core.spec.style.FunSpec
import io.kotest.datatest.withData
import io.kotest.matchers.string.shouldStartWith
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlin.random.Random
import kotlin.time.Duration.Companion.seconds
@@ -38,17 +35,13 @@ class BusTest :
)
withData(list) { bus ->
val spy = spyk<() -> Unit>()
bus.subscribe { obj ->
spy()
obj.value shouldStartWith "testMessage"
}
bus.publish(ObjTest("testMessage${Random.nextInt()}"))
bus.publish(ObjTest("testMessage${Random.nextInt()}"))
eventually(1.seconds) {
verify(exactly = 2) { spy() }
spyPing(exactly = 2, duration = 1.seconds) { ping ->
bus.subscribe { obj ->
ping()
obj.value shouldStartWith "testMessage"
}
bus.publish(ObjTest("testMessage${Random.nextInt()}"))
bus.publish(ObjTest("testMessage${Random.nextInt()}"))
}
}
}
@@ -0,0 +1,8 @@
package eventDemo.libs.command
import kotlinx.serialization.Serializable
@Serializable
data class CommandForTest(
override val id: CommandId,
) : Command
@@ -1,45 +0,0 @@
package eventDemo.libs.command
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.core.spec.style.FunSpec
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlin.time.Duration.Companion.seconds
@Serializable
class CommandTest(
override val id: CommandId,
) : Command
@OptIn(DelicateCoroutinesApi::class)
class CommandStreamChannelTest :
FunSpec({
test("send and receive") {
val command = CommandTest(CommandId())
val channel = Channel<CommandTest>()
val stream = CommandStreamChannel(CommandRunnerController())
val spy = spyk<() -> Unit>()
GlobalScope.launch {
stream.process(channel) {
println("In action ${it.id}")
spy()
}
}
channel.send(command)
eventually(3.seconds) {
verify(exactly = 1) { spy() }
}
}
})
@@ -0,0 +1,34 @@
package eventDemo.libs.command
import eventDemo.testHelpers.spyPing
import io.kotest.core.spec.style.FunSpec
import org.junit.jupiter.api.assertThrows
import kotlin.time.Duration.Companion.seconds
class CommandUnicityCheckerTest :
FunSpec({
test("runOnlyOnce must run all commands") {
spyPing(exactly = 2, duration = 3.seconds) { ping ->
val com1 = CommandForTest(CommandId())
val com2 = CommandForTest(CommandId())
CommandUnicityChecker<CommandForTest>().run {
runOnlyOnce(com1) { ping() }
runOnlyOnce(com2) { ping() }
}
}
}
test("runOnlyOnce") {
spyPing(exactly = 2, duration = 3.seconds) { ping ->
val com1 = CommandForTest(CommandId())
val com2 = CommandForTest(CommandId())
CommandUnicityChecker<CommandForTest>().run {
runOnlyOnce(com1) { ping() }
runOnlyOnce(com2) { ping() }
assertThrows<CommandUnicityChecker.UnicityException> {
runOnlyOnce(com2) { ping() }
}
}
}
}
})
@@ -1,38 +0,0 @@
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,48 +0,0 @@
package eventDemo.libs.event
import eventDemo.Tag
import io.kotest.common.KotestInternal
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
@OptIn(DelicateCoroutinesApi::class, KotestInternal::class)
class VersionBuilderLocalTest :
FunSpec({
test("buildNextVersion") {
VersionBuilderLocal().run {
val id = IdTest()
buildNextVersion(id) shouldBeEqual 1
buildNextVersion(id) shouldBeEqual 2
buildNextVersion(IdTest()) shouldBeEqual 1
buildNextVersion(id) shouldBeEqual 3
}
}
test("buildNextVersion concurrently").config(tags = setOf(Tag.Concurrence)) {
val versionBuilder = VersionBuilderLocal()
val id = IdTest()
(1..20)
.map {
GlobalScope.launch {
(1..1000).map {
versionBuilder.buildNextVersion(id)
}
}
}.joinAll()
versionBuilder.getLastVersion(id) shouldBeEqual 20 * 1000
}
test("getLastVersion") {
VersionBuilderLocal().run {
val id = IdTest()
getLastVersion(id) shouldBeEqual 0
getLastVersion(id) shouldBeEqual 0
getLastVersion(id) shouldBeEqual 0
}
}
})
@@ -1,225 +0,0 @@
package eventDemo.libs.event.projection
import eventDemo.cleanProjections
import eventDemo.configuration.serializer.UUIDSerializer
import eventDemo.libs.event.AggregateId
import eventDemo.libs.event.Event
import eventDemo.libs.event.EventStore
import eventDemo.libs.event.EventStoreInMemory
import eventDemo.libs.event.VersionBuilderLocal
import io.kotest.assertions.nondeterministic.continually
import io.kotest.core.spec.style.FunSpec
import io.kotest.datatest.withData
import io.kotest.engine.names.WithDataTestName
import io.kotest.matchers.equals.shouldBeEqual
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
import kotlinx.serialization.json.Json
import redis.clients.jedis.JedisPooled
import java.util.UUID
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds
@OptIn(DelicateCoroutinesApi::class)
class ProjectionRepositoryTest :
FunSpec({
data class TestData(
val store: EventStore<TestEvents, IdTest>,
val repository: ProjectionRepository<TestEvents, ProjectionTest, IdTest>,
) : WithDataTestName {
override fun dataTestName(): String =
"${repository::class.simpleName} with ${store::class.simpleName}"
}
val eventStores =
listOf(
{ EventStoreInMemory<TestEvents, IdTest>() },
)
val projectionRepo =
listOf(
::getRepoInMemoryTest,
::getRepoInRedisTest,
)
val list =
eventStores.flatMap { store ->
projectionRepo.map { repo ->
TestData(store(), repo())
}
}
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)
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.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.applyAndSave(event2)
assertNotNull(repo.get(event2.aggregateId)).also {
assertNotNull(it.value) shouldBeEqual "val1val2"
}
}
}
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.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.applyAndSave(event1)
val event2 = Event2Test(value2 = "val2", version = 2, aggregateId = aggregateId)
eventStore.publish(event2)
repo.applyAndSave(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("ProjectionRepository should be thread safe") {
continually(1.seconds) {
withData(list) { (eventStore, repo) ->
val aggregateId = IdTest()
val versionBuilder = VersionBuilderLocal()
val lock = ReentrantLock()
(0..9)
.map {
GlobalScope.launch {
repeat(10) {
lock.withLock {
runBlocking {
EventXTest(
num = 1,
version = versionBuilder.buildNextVersion(aggregateId),
aggregateId = aggregateId,
).also { repo.applyAndSave(it) }
}
}
}
}
}.joinAll()
assertNotNull(repo.get(aggregateId)).lastEventVersion shouldBeEqual 100
assertNotNull(repo.get(aggregateId)).num shouldBeEqual 100
}
}
}
})
@JvmInline
@Serializable
private value class IdTest(
@Serializable(with = UUIDSerializer::class)
override val id: UUID = UUID.randomUUID(),
) : AggregateId
@Serializable
private data class ProjectionTest(
override val aggregateId: IdTest,
override val lastEventVersion: Int = 0,
var value: String? = null,
var num: Int = 0,
) : Projection<IdTest>
private sealed interface TestEvents : Event<IdTest>
private data class Event1Test(
override val eventId: UUID = UUID.randomUUID(),
override val aggregateId: IdTest,
override val createdAt: Instant = Clock.System.now(),
override val version: Int,
val value1: String,
) : TestEvents
private data class Event2Test(
override val eventId: UUID = UUID.randomUUID(),
override val aggregateId: IdTest,
override val createdAt: Instant = Clock.System.now(),
override val version: Int,
val value2: String,
) : TestEvents
private data class EventXTest(
override val eventId: UUID = UUID.randomUUID(),
override val aggregateId: IdTest,
override val createdAt: Instant = Clock.System.now(),
override val version: Int,
val num: Int,
) : TestEvents
private fun getRepoInMemoryTest(): ProjectionRepository<TestEvents, ProjectionTest, IdTest> =
ProjectionRepositoryInMemory(
initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) },
applyToProjection = apply,
)
private fun getRepoInRedisTest(): ProjectionRepository<TestEvents, ProjectionTest, IdTest> {
val jedis = JedisPooled("redis://localhost:6379")
jedis.cleanProjections()
return ProjectionRepositoryInRedis(
jedis = jedis,
initialStateBuilder = { aggregateId: IdTest -> ProjectionTest(aggregateId) },
projectionClass = ProjectionTest::class,
projectionToJson = { Json.encodeToString(it) },
jsonToProjection = { Json.decodeFromString(it) },
applyToProjection = apply,
)
}
private val apply: ProjectionTest.(TestEvents) -> ProjectionTest = { event ->
this.let { projection ->
when (event) {
is Event1Test -> {
projection.copy(value = (projection.value.orEmpty()) + event.value1)
}
is Event2Test -> {
projection.copy(value = (projection.value.orEmpty()) + event.value2)
}
is EventXTest -> {
projection.copy(num = projection.num + event.num)
}
}.copy(
lastEventVersion = event.version,
)
}
}
@@ -1,8 +1,11 @@
package eventDemo.libs.event
package eventDemo.libs.eventSource
import eventDemo.Tag
import eventDemo.testKoinApplicationWithConfig
import io.kotest.common.KotestInternal
import eventDemo.libs.eventSource.eventStore.EventStream
import eventDemo.libs.eventSource.eventStore.EventStreamInMemory
import eventDemo.libs.eventSource.eventStore.EventStreamInPostgresql
import eventDemo.libs.eventSource.eventStore.EventStreamPublishException
import eventDemo.testHelpers.testKoinApplicationWithConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.datatest.withData
import io.kotest.matchers.collections.shouldHaveSize
@@ -17,28 +20,30 @@ import org.junit.jupiter.api.assertThrows
import org.koin.core.Koin
import kotlin.test.assertNotNull
@OptIn(KotestInternal::class, DelicateCoroutinesApi::class)
@OptIn(DelicateCoroutinesApi::class)
class EventStreamTest :
FunSpec({
tags(Tag.Postgresql)
fun EventStream<EventXTest, IdTest>.with3Events(block: EventStream<EventXTest, IdTest>.(id: IdTest) -> Unit) =
also {
publish(EventXTest(aggregateId = aggregateId, version = 1, num = 1))
publish(EventXTest(aggregateId = aggregateId, version = 2, num = 2))
publish(EventXTest(aggregateId = aggregateId, version = 3, num = 3))
append(EventXTest(aggregateId = aggregateId, version = 1, num = 1))
append(EventXTest(aggregateId = aggregateId, version = 2, num = 2))
append(EventXTest(aggregateId = aggregateId, version = 3, num = 3))
block(aggregateId)
}
fun Koin.eventStreams(): List<EventStream<EventXTest, IdTest>> =
listOf(
EventStreamInMemory(IdTest()),
EventStreamInPostgresql(
IdTest(),
dataSource = get(),
objectToString = { Json.encodeToString(it) },
stringToObject = { Json.decodeFromString(it) },
),
fun Koin.eventStreams(): Map<String, EventStream<EventXTest, IdTest>> =
mapOf(
EventStreamInMemory::class.simpleName.toString() to EventStreamInMemory(IdTest()),
EventStreamInPostgresql::class.simpleName.toString() to
EventStreamInPostgresql(
IdTest(),
dataSource = get(),
objectToString = { Json.encodeToString(it) },
stringToObject = { Json.decodeFromString(it) },
"game.game_event_stream",
),
)
context("readVersionBetween should only return the event of aggregate") {
@@ -98,7 +103,7 @@ class EventStreamTest :
context("publish should be throw error when publish another aggregate event") {
testKoinApplicationWithConfig {
withData(eventStreams()) {
assertThrows<EventStreamPublishException> { it.publish(EventXTest(aggregateId = IdTest(), version = 1, num = 1)) }
assertThrows<EventStreamPublishException> { it.append(EventXTest(aggregateId = IdTest(), version = 1, num = 1)) }
}
}
}
@@ -110,7 +115,7 @@ class EventStreamTest :
.map { i1 ->
GlobalScope.launch {
(1..10).forEach { i2 ->
stream.publish(
stream.append(
EventXTest(
aggregateId = stream.aggregateId,
version = (i1 * 10) + i2,
@@ -1,6 +1,7 @@
package eventDemo.libs.event
package eventDemo.libs.eventSource
import eventDemo.configuration.serializer.UUIDSerializer
import eventDemo.contexts.game.infrastructure.persistence.serializers.EventIdSerializer
import eventDemo.libs.serializer.UUIDSerializer
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
@@ -18,8 +19,8 @@ sealed interface TestEvents : Event<IdTest>
@Serializable
data class EventXTest(
@Serializable(with = UUIDSerializer::class)
override val eventId: UUID = UUID.randomUUID(),
@Serializable(with = EventIdSerializer::class)
override val eventId: EventId = EventId(),
override val aggregateId: IdTest,
override val createdAt: Instant = Clock.System.now(),
override val version: Int,
@@ -1,6 +1,6 @@
package eventDemo.libs
package eventDemo.libs.helpers
import eventDemo.libs.command.Command
import eventDemo.libs.command.CommandForTest
import eventDemo.libs.command.CommandId
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
@@ -8,15 +8,9 @@ import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import java.util.UUID
import kotlin.test.assertIs
@Serializable
data class CommandTest(
override val id: CommandId,
) : Command
class FrameChannelConverterTest :
FunSpec({
@@ -28,7 +22,7 @@ class FrameChannelConverterTest :
val channel = Channel<Frame>()
launch {
val commandChannel = toObjectChannel<CommandTest>(channel)
val commandChannel = toObjectChannel<CommandForTest>(channel)
commandChannel.receive().id shouldBeEqual id
channel.close()
}
@@ -39,13 +33,13 @@ class FrameChannelConverterTest :
test("fromFrameChannel") {
val uuid = "d737c631-76af-406e-bc29-f3e5b97226a5"
val id = CommandId(UUID.fromString(uuid))
val command = CommandTest(id)
val command = CommandForTest(id)
val jsonCommand = """{"id":"$uuid"}"""
val channel = Channel<Frame>()
launch {
val commandChannel = fromFrameChannel<CommandTest>(channel)
val commandChannel = fromFrameChannel<CommandForTest>(channel)
commandChannel.send(command)
commandChannel.close()
}
@@ -0,0 +1,63 @@
package eventDemo.testHelpers
import eventDemo.contexts.auth.domain.User
import eventDemo.contexts.game.application.command.handlers.GameCommandHandlerDispatcher
import eventDemo.contexts.game.application.command.models.JoinTheGameCommand
import eventDemo.contexts.game.application.command.models.PlayCardCommand
import eventDemo.contexts.game.application.command.models.ReadyToPlayCommand
import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.domain.game.Card
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player
import org.koin.core.Koin
import java.util.UUID
object CreateGameWithCommandsHelpers {
class Data(
private val repo: GameRepository,
val gameId: GameId,
) {
fun getPlayer(user: User): Player =
repo.get(gameId)!!.players.get(user.id)
}
context(koin: Koin)
fun <T> createGameWithCommands(
gameName: String = "testGame${UUID.randomUUID()}",
block: context(CreateGameWithCommandsHelpers, GameCommandHandlerDispatcher) Data.() -> T,
): T {
val gameId = GameId(UUID.nameUUIDFromBytes(gameName.encodeToByteArray()))
val repo = koin.get<GameRepository>()
repo.create(gameId)
return koin.get<GameCommandHandlerDispatcher>().run {
with(CreateGameWithCommandsHelpers) {
Data(repo, gameId).block()
}
}
}
context(dispatcher: GameCommandHandlerDispatcher, data: Data)
fun User.joinTheGame(): JoinTheGameCommand =
JoinTheGameCommand(
id,
JoinTheGameCommand.Payload(data.gameId),
).also { dispatcher.dispatch(it) }
context(dispatcher: GameCommandHandlerDispatcher, data: Data)
fun Player.readyToPlay(): ReadyToPlayCommand =
ReadyToPlayCommand(
userId,
ReadyToPlayCommand.Payload(data.gameId, id),
).also { dispatcher.dispatch(it) }
context(dispatcher: GameCommandHandlerDispatcher, data: Data)
fun Player.playCard(
card: Card,
chosenColor: Card.Color? = null,
): PlayCardCommand =
PlayCardCommand(
userId,
PlayCardCommand.Payload(data.gameId, id, card, chosenColor),
).also { dispatcher.dispatch(it) }
}
@@ -0,0 +1,78 @@
package eventDemo.testHelpers
import eventDemo.contexts.auth.domain.User
import eventDemo.contexts.game.application.command.models.GameCommand
import eventDemo.contexts.game.application.command.models.JoinTheGameCommand
import eventDemo.contexts.game.application.command.models.PlayCardCommand
import eventDemo.contexts.game.application.command.models.ReadyToPlayCommand
import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.domain.game.Card
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.domain.game.gameState.Game
import kotlinx.coroutines.channels.Channel
import org.koin.core.Koin
object CreateGameWithCommandsInChannelsHelpers {
class Data(
private val repo: GameRepository,
val gameId: GameId,
val currentUser: User,
) {
fun getPlayer(user: User): Player =
game.players.get(user.id)
val currentPlayer: Player get() = getPlayer(currentUser)
val game: Game
get() = repo.get(gameId)!!
}
context(koin: Koin)
suspend fun <T> createGameWithCommandsInChannels(
channelCommand: Channel<GameCommand>,
gameId: GameId,
user: User,
block:
suspend context(
CreateGameWithCommandsInChannelsHelpers,
Channel<GameCommand>,
User,
) Data.() -> T,
): T {
val repo = koin.get<GameRepository>()
repo.getOrCreate(gameId)
return with(channelCommand) {
with(user) {
with(CreateGameWithCommandsInChannelsHelpers) {
Data(repo, gameId, user).block()
}
}
}
}
context(channelCommand: Channel<GameCommand>, data: Data)
suspend fun joinTheGame(): JoinTheGameCommand =
JoinTheGameCommand(
data.currentUser.id,
JoinTheGameCommand.Payload(data.gameId),
).also { channelCommand.send(it) }
context(channelCommand: Channel<GameCommand>, data: Data)
suspend fun readyToPlay(): ReadyToPlayCommand =
ReadyToPlayCommand(
data.currentUser.id,
ReadyToPlayCommand.Payload(data.gameId, data.currentPlayer.id),
).also { channelCommand.send(it) }
context(channelCommand: Channel<GameCommand>, data: Data)
suspend fun playCard(
card: Card,
chosenColor: Card.Color? = null,
): PlayCardCommand =
PlayCardCommand(
data.currentUser.id,
PlayCardCommand.Payload(data.gameId, data.currentPlayer.id, card, chosenColor),
).also { channelCommand.send(it) }
}
@@ -1,4 +1,4 @@
package eventDemo
package eventDemo.testHelpers
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
@@ -0,0 +1,7 @@
package eventDemo.testHelpers
import eventDemo.contexts.auth.domain.User
fun createNewUser(name: String): User =
User
.createNewUser(name, "changeit")
@@ -0,0 +1,6 @@
package eventDemo.testHelpers
import eventDemo.contexts.game.domain.game.gameState.GameStarted
fun GameStarted.allCardCount(): Int =
drawPile.remainingCards + discardPile.size + players.map { it.hand }.sumOf { it.size }
@@ -1,11 +1,10 @@
package eventDemo
package eventDemo.testHelpers
import com.zaxxer.hikari.HikariDataSource
import eventDemo.domain.entity.Card
import eventDemo.domain.entity.Deck
import eventDemo.configuration.domain.configureGameListener
import eventDemo.configuration.injection.appKoinModule
import eventDemo.configuration.ktor.configuration
import eventDemo.configuration.appKoinModule
import eventDemo.configuration.configuration
import eventDemo.contexts.game.infrastructure.configuration.listener.configureProjectionListener
import eventDemo.contexts.game.infrastructure.configuration.listener.configureReactionListener
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.server.config.ApplicationConfig
import io.ktor.server.testing.ApplicationTestBuilder
@@ -15,22 +14,15 @@ import org.koin.core.Koin
import org.koin.core.module.KoinApplicationDslMarker
import org.koin.dsl.koinApplication
import org.koin.ktor.ext.getKoin
import redis.clients.jedis.UnifiedJedis
import javax.sql.DataSource
fun Deck.allCardCount(): Int =
stack.size + discard.size + playersHands.values.flatten().size
fun Deck.allCards(): Set<Card> =
stack + discard + playersHands.values.flatten()
@KoinApplicationDslMarker
suspend fun <T> testKoinApplicationWithConfig(block: suspend Koin.() -> T): T =
koinApplication { modules(appKoinModule(ApplicationConfig("application.conf").configuration())) }
koinApplication { modules(appKoinModule(ApplicationConfig("application.conf").configuration)) }
.koin
.run {
cleanDataTest()
configureGameListener()
configureProjectionListener()
configureReactionListener()
block()
.apply { get<HikariDataSource>().close() }
}
@@ -51,34 +43,12 @@ fun testApplicationWithConfig(
logger.info { "Config App" }
val koin = getKoin()
koin.cleanDataTest()
runCatching {
logger.info { "Starting A" }
configBuilder(koin)
logger.info { "A finish" }
}
logger.info { "Starting A" }
configBuilder(koin)
logger.info { "A finish" }
}
logger.info { "Starting B" }
this@testApplication.block()
logger.info { "B finish" }
}
}
fun DataSource.cleanEventSource() {
this.connection.use {
it
.prepareStatement(
"""
truncate event_stream;
""".trimIndent(),
).execute()
}
}
fun UnifiedJedis.cleanProjections() {
flushAll()
}
fun Koin.cleanDataTest() {
get<DataSource>().cleanEventSource()
get<UnifiedJedis>().cleanProjections()
}
@@ -0,0 +1,27 @@
package eventDemo.testHelpers
import org.koin.core.Koin
import redis.clients.jedis.UnifiedJedis
import javax.sql.DataSource
fun DataSource.cleanEventSource() {
this.connection.use {
it
.prepareStatement(
"""
truncate game.game_event_stream;
truncate auth.user_event_stream;
truncate auth.user;
""".trimIndent(),
).execute()
}
}
fun UnifiedJedis.cleanProjections() {
flushAll()
}
fun Koin.cleanDataTest() {
get<DataSource>().cleanEventSource()
get<UnifiedJedis>().cleanProjections()
}
@@ -0,0 +1,33 @@
package eventDemo.testHelpers
import io.kotest.assertions.nondeterministic.eventually
import io.mockk.spyk
import io.mockk.verify
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
inline fun <R> arrange(block: () -> R): R =
run(block)
inline fun <T, R> T.and(block: (T) -> R): R =
this.let(block)
inline fun <T, R> T.act(block: (T) -> R): R =
this.let(block)
inline fun <T, R> T.assert(block: (T) -> R): R =
this.let(block)
suspend fun spyPing(
duration: Duration = 0.5.seconds,
exactly: Int = -1,
block: (ping: () -> Unit) -> Unit,
) {
val spy = spyk<() -> Unit>()
block(spy)
eventually(duration) {
verify(exactly = exactly) { spy() }
}
}