chore: upgrade versions and rename folders

This commit is contained in:
2026-07-20 19:54:58 +02:00
parent 7fb488ab56
commit 78956ce84e
113 changed files with 471 additions and 470 deletions
@@ -0,0 +1,10 @@
package eventDemo.adapter.presenter.query
import eventDemo.domain.entity.Player
import eventDemo.configuration.ktor.makeJwt
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.header
internal fun HttpRequestBuilder.withAuth(player: Player) {
header("Authorization", "Bearer ${player.makeJwt("secret")}")
}
@@ -0,0 +1,115 @@
package eventDemo.adapter.presenter.query
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.event.GameEventHandler
import eventDemo.domain.event.event.GameStartedEvent
import eventDemo.domain.event.event.NewPlayerEvent
import eventDemo.domain.event.event.PlayerReadyEvent
import eventDemo.domain.event.projection.GameList
import eventDemo.testApplicationWithConfig
import io.github.oshai.kotlinlogging.KotlinLogging
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.equals.shouldBeEqual
import io.ktor.client.call.body
import io.ktor.client.request.accept
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
val logger = KotlinLogging.logger {}
class GameListRouteTest :
FunSpec({
test("/games with no game started") {
testApplicationWithConfig {
val player1 = Player(name = "Nikola")
logger.info { "Starting player1" }
httpClient()
.get("/games") {
withAuth(player1)
accept(ContentType.Application.Json)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
val list = call.body<List<GameList>>()
assertTrue(list.isEmpty())
}
}
}
test("/games return a game with status OPENING") {
val gameId = GameId()
val player1 = Player(name = "Nikola")
testApplicationWithConfig(
{
get<GameEventHandler>()
.handle(gameId) {
NewPlayerEvent(gameId, player1, it)
}
},
) {
// Wait until the projection is created
eventually(10.seconds) {
httpClient()
.get("/games") {
withAuth(player1)
accept(ContentType.Application.Json)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
call.body<List<GameList>>().first().let {
it.status shouldBeEqual GameList.Status.OPENING
it.players shouldHaveSize 1
it.players shouldContain player1
it.winners shouldHaveSize 0
}
}
}
}
}
test("/games return a game with status IS_STARTED") {
val gameId = GameId()
val player1 = Player(name = "Nikola")
val player2 = Player(name = "Einstein")
testApplicationWithConfig({
val eventHandler = get<GameEventHandler>()
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) }
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) }
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) }
eventHandler.handle(gameId) {
GameStartedEvent.new(
gameId,
setOf(player1, player2),
it,
shuffleIsDisabled = true,
)
}
}) {
eventually(3.seconds) {
httpClient()
.get("/games") {
withAuth(player1)
accept(ContentType.Application.Json)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
call.body<List<GameList>>().first().let {
it.status shouldBeEqual GameList.Status.IS_STARTED
it.players shouldHaveSize 2
it.players shouldContain player1
it.players shouldContain player2
it.winners shouldHaveSize 0
}
}
}
}
}
})
@@ -0,0 +1,196 @@
package eventDemo.adapter.presenter.query
import eventDemo.Tag
import eventDemo.domain.command.GameCommandHandler
import eventDemo.domain.command.command.GameCommand
import eventDemo.domain.command.command.IWantToJoinTheGameCommand
import eventDemo.domain.command.command.IWantToPlayCardCommand
import eventDemo.domain.command.command.IamReadyToPlayCommand
import eventDemo.domain.entity.Card
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.event.event.disableShuffleDeck
import eventDemo.domain.event.projection.GameState
import eventDemo.domain.event.projection.GameStateRepository
import eventDemo.domain.event.projection.projectionListener.PlayerNotificationListener
import eventDemo.domain.notification.CommandSuccessNotification
import eventDemo.domain.notification.ItsTheTurnOfNotification
import eventDemo.domain.notification.Notification
import eventDemo.domain.notification.PlayerAsJoinTheGameNotification
import eventDemo.domain.notification.PlayerAsPlayACardNotification
import eventDemo.domain.notification.PlayerWasReadyNotification
import eventDemo.domain.notification.TheGameWasStartedNotification
import eventDemo.domain.notification.WelcomeToTheGameNotification
import eventDemo.testKoinApplicationWithConfig
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.assertions.nondeterministic.until
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.equals.shouldBeEqual
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
@DelicateCoroutinesApi
class GameSimulationTest :
FunSpec({
tags(Tag.Postgresql)
test("Simulation of a game") {
withTimeout(10.seconds) {
disableShuffleDeck()
val gameId = GameId()
val player1 = Player(name = "Nikola")
val player2 = Player(name = "Einstein")
val channelCommand1 = Channel<GameCommand>(Channel.BUFFERED)
val channelCommand2 = Channel<GameCommand>(Channel.BUFFERED)
val channelNotification1 = Channel<Notification>(Channel.BUFFERED)
val channelNotification2 = Channel<Notification>(Channel.BUFFERED)
var playedCard1: Card? = null
var playedCard2: Card? = null
var player1HasJoin = false
testKoinApplicationWithConfig {
val commandHandler = get<GameCommandHandler>()
val playerNotificationListener = get<PlayerNotificationListener>()
val gameStateRepository = get<GameStateRepository>()
// Run command handler
// In the normal process, these handlers is invoque players connect to the websocket
run {
GlobalScope.launch(Dispatchers.IO) {
commandHandler.handleIncomingPlayerCommands(player1, gameId, channelCommand1, channelNotification1)
}
GlobalScope.launch(Dispatchers.IO) {
commandHandler.handleIncomingPlayerCommands(player2, gameId, channelCommand2, channelNotification2)
}
}
// Consume etch notification of players, and put theses in a list.
// Is used later to control when other players can execute the next action
val player1Notifications = mutableListOf<Notification>()
val player2Notifications = mutableListOf<Notification>()
run {
GlobalScope.launch {
for (notification in channelNotification1) {
player1Notifications.add(notification)
}
}
GlobalScope.launch {
for (notification in channelNotification2) {
player2Notifications.add(notification)
}
}
}
// Player 1 actions
val player1Job =
launch {
playerNotificationListener.startListening(player1, gameId) {
channelNotification1.trySendBlocking(it)
}
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player1)).also { sendCommand ->
channelCommand1.send(sendCommand)
player1Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
}
player1HasJoin = true
player1Notifications.waitNotification<WelcomeToTheGameNotification> { players == setOf(player1) }
player1Notifications.waitNotification<PlayerAsJoinTheGameNotification> { player == player2 }
IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(gameId, player1)).also { sendCommand ->
channelCommand1.send(sendCommand)
player1Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
}
player1Notifications.waitNotification<PlayerWasReadyNotification> { player == player2 }
val player1Hand = player1Notifications.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }.hand
playedCard1 = player1Hand.first()
player1Notifications.waitNotification<ItsTheTurnOfNotification> { player == player1 }
IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(gameId, player1, player1Hand.first())).also { sendCommand ->
channelCommand1.send(sendCommand)
player1Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
}
player1Notifications.waitNotification<ItsTheTurnOfNotification> { player == player2 }
player1Notifications.waitNotification<PlayerAsPlayACardNotification> {
player == player2 && card == playedCard2
}
}
// Player 2 actions
val player2Job =
launch {
// wait player 1 has joined the game
until(3.seconds) { player1HasJoin }
playerNotificationListener.startListening(player2, gameId) {
channelNotification2.trySendBlocking(it)
}
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player2)).also { sendCommand ->
channelCommand2.send(sendCommand)
player2Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
}
player2Notifications.waitNotification<WelcomeToTheGameNotification> { players == setOf(player1, player2) }
player2Notifications.waitNotification<PlayerWasReadyNotification> { player == player1 }
IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(gameId, player2)).also { sendCommand ->
channelCommand2.send(sendCommand)
player2Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
}
val player2Hand =
player2Notifications.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }.hand
player2Notifications.waitNotification<ItsTheTurnOfNotification> { player == player1 }
player2Notifications.waitNotification<PlayerAsPlayACardNotification> {
player == player1 && card == playedCard1
}
playedCard2 = player2Hand.first()
player2Notifications.waitNotification<ItsTheTurnOfNotification> { player == player2 }
IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(gameId, player2, player2Hand.first())).also { sendCommand ->
channelCommand2.send(sendCommand)
player2Notifications.waitNotification<CommandSuccessNotification> { commandId == sendCommand.id }
}
}
// Wait the end of the game
joinAll(player1Job, player2Job)
// Build the last state from the event store
val state = gameStateRepository.get(gameId)
// Check if the state is correct
state.aggregateId shouldBeEqual gameId
assertTrue(state.isStarted)
state.players shouldBeEqual setOf(player1, player2)
state.readyPlayers shouldBeEqual setOf(player1, player2)
state.direction shouldBeEqual GameState.Direction.CLOCKWISE
assertNotNull(state.lastCardPlayer) shouldBeEqual player2
assertNotNull(state.cardOnCurrentStack) shouldBeEqual assertNotNull(playedCard2)
}
}
}
})
private suspend inline fun <reified T : Notification> MutableList<Notification>.waitNotification(crossinline block: T.() -> Boolean): T =
eventually(3.seconds) {
filterIsInstance<T>().first { block(it) }
}
@@ -0,0 +1,157 @@
package eventDemo.adapter.presenter.query
import eventDemo.domain.entity.Card
import eventDemo.domain.entity.GameId
import eventDemo.domain.entity.Player
import eventDemo.domain.event.GameEventHandler
import eventDemo.domain.event.event.CardIsPlayedEvent
import eventDemo.domain.event.event.NewPlayerEvent
import eventDemo.domain.event.event.PlayerReadyEvent
import eventDemo.domain.event.event.disableShuffleDeck
import eventDemo.domain.event.projection.GameState
import eventDemo.domain.event.projection.GameStateRepository
import eventDemo.testApplicationWithConfig
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.assertions.nondeterministic.until
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.equals.shouldBeEqual
import io.ktor.client.call.body
import io.ktor.client.request.accept
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.runBlocking
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.time.Duration.Companion.seconds
class GameStateRouteTest :
FunSpec({
test("""The route "/games/{id}/state" should returns a non started game when is not exist""") {
testApplicationWithConfig {
val gameId = GameId()
val player1 = Player(name = "Nikola")
httpClient()
.get("/games/$gameId/state") {
withAuth(player1)
accept(ContentType.Application.Json)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
call.body<GameState>().apply {
aggregateId shouldBeEqual gameId
players shouldHaveSize 0
isStarted shouldBeEqual false
}
}
}
}
test("""The route "/games/{id}/state" should returns the state with all informations""") {
val gameId = GameId()
val player1 = Player(name = "Nikola")
val player2 = Player(name = "Einstein")
testApplicationWithConfig({
disableShuffleDeck()
val eventHandler = get<GameEventHandler>()
val stateRepo = get<GameStateRepository>()
runBlocking {
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) }
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) }
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) }
val lastPlayedCard = eventually(3.seconds) { stateRepo.get(gameId).playableCards(player1).first() }
assertIs<Card.NumericCard>(lastPlayedCard)
.let {
it.number shouldBeEqual 0
it.color shouldBeEqual Card.Color.Red
}
eventHandler.handle(gameId) {
CardIsPlayedEvent(
gameId,
lastPlayedCard,
player1,
it,
)
}
until(3.seconds) {
stateRepo
.get(gameId)
.deck.discard
.last() == lastPlayedCard
}
}
}) {
httpClient()
.get("/games/$gameId/state") {
withAuth(player1)
accept(ContentType.Application.Json)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
call.body<GameState>().apply {
aggregateId shouldBeEqual gameId
players shouldHaveSize 2
isStarted shouldBeEqual true
assertIs<CardIsPlayedEvent>(lastEvent)
readyPlayers shouldBeEqual setOf(player1, player2)
direction shouldBeEqual GameState.Direction.CLOCKWISE
assertNotNull(lastCardPlayer) shouldBeEqual player1
assertNotNull(colorOnCurrentStack) shouldBeEqual Card.Color.Red
}
}
}
}
test("""The route "/games/{id}/card/last" should return the last card played of the game""") {
val gameId = GameId()
val player1 = Player(name = "Nikola")
val player2 = Player(name = "Einstein")
var lastPlayedCard: Card? = null
testApplicationWithConfig({
disableShuffleDeck()
val eventHandler = get<GameEventHandler>()
val stateRepo = get<GameStateRepository>()
runBlocking {
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) }
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) }
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) }
lastPlayedCard = eventually(3.seconds) { stateRepo.get(gameId).playableCards(player1).first() }
assertIs<Card.NumericCard>(lastPlayedCard)
.let {
it.number shouldBeEqual 0
it.color shouldBeEqual Card.Color.Red
}
eventHandler.handle(gameId) {
CardIsPlayedEvent(
gameId,
assertNotNull(lastPlayedCard),
player1,
it,
)
}
until(3.seconds) {
stateRepo
.get(gameId)
.deck.discard
.last() == lastPlayedCard
}
}
}) {
httpClient()
.get("/games/$gameId/card/last") {
withAuth(player1)
accept(ContentType.Application.Json)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
assertEquals(assertNotNull(lastPlayedCard), call.body<Card>())
}
}
}
})
@@ -0,0 +1,15 @@
package eventDemo.adapter.presenter.query
import eventDemo.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(),
)
}
}