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
@@ -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")}")
}
@@ -0,0 +1,16 @@
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
import io.ktor.server.testing.ApplicationTestBuilder
fun ApplicationTestBuilder.httpClient(): HttpClient =
createClient {
install(ContentNegotiation) {
json(
defaultJsonSerializer(),
)
}
}
@@ -0,0 +1,28 @@
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 javax.sql.DataSource
class PostgresqlTest :
FunSpec({
tags(Tag.Postgresql)
test("test connection with PostgreSQL") {
testKoinApplicationWithConfig {
get<DataSource>().connection.use { connection ->
connection
.prepareStatement(
"""
select 1;
""".trimIndent(),
).execute()
.let {
it shouldBeEqual true
}
}
}
}
})
@@ -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
}
}
}
}
}
})