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