refactoring: Masive refactor to build the V2
This commit is contained in:
@@ -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}") }
|
||||
}
|
||||
+121
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
+85
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
+301
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+28
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
+60
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
+21
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
+44
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user