Fix: close all connections after each tests
This commit is contained in:
@@ -48,6 +48,12 @@ private fun DefaultWebSocketServerSession.runWebSocket(
|
|||||||
val currentPlayer = call.getPlayer()
|
val currentPlayer = call.getPlayer()
|
||||||
val outgoingFrameChannel: SendChannel<Notification> = fromFrameChannel(outgoing)
|
val outgoingFrameChannel: SendChannel<Notification> = fromFrameChannel(outgoing)
|
||||||
withLoggingContext("currentPlayer" to currentPlayer.toString()) {
|
withLoggingContext("currentPlayer" to currentPlayer.toString()) {
|
||||||
|
val notificationListener =
|
||||||
|
playerNotificationListener.startListening(
|
||||||
|
currentPlayer,
|
||||||
|
gameId,
|
||||||
|
) { outgoingFrameChannel.trySendBlocking(it) }
|
||||||
|
|
||||||
// TODO change GlobalScope
|
// TODO change GlobalScope
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
commandHandler.handle(
|
commandHandler.handle(
|
||||||
@@ -56,12 +62,8 @@ private fun DefaultWebSocketServerSession.runWebSocket(
|
|||||||
toObjectChannel(incoming),
|
toObjectChannel(incoming),
|
||||||
outgoingFrameChannel,
|
outgoingFrameChannel,
|
||||||
)
|
)
|
||||||
|
notificationListener.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
playerNotificationListener.startListening(
|
|
||||||
currentPlayer,
|
|
||||||
gameId,
|
|
||||||
) { outgoingFrameChannel.trySendBlocking(it) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ class PlayerNotificationListener(
|
|||||||
currentPlayer: Player,
|
currentPlayer: Player,
|
||||||
gameId: GameId,
|
gameId: GameId,
|
||||||
outgoingNotification: (Notification) -> Unit,
|
outgoingNotification: (Notification) -> Unit,
|
||||||
) {
|
): AutoCloseable {
|
||||||
projectionBus.subscribe { currentState ->
|
return projectionBus.subscribe { currentState ->
|
||||||
if (currentState !is GameState) return@subscribe
|
if (currentState !is GameState) return@subscribe
|
||||||
if (currentState.aggregateId != gameId) return@subscribe
|
if (currentState.aggregateId != gameId) return@subscribe
|
||||||
withLoggingContext("projection" to currentState.toString()) {
|
withLoggingContext("projection" to currentState.toString()) {
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ import eventDemo.business.event.projection.gameState.GameStateRepository
|
|||||||
import eventDemo.libs.event.projection.SnapshotConfig
|
import eventDemo.libs.event.projection.SnapshotConfig
|
||||||
import org.koin.core.module.Module
|
import org.koin.core.module.Module
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.core.scope.Scope
|
||||||
|
import org.koin.core.scope.ScopeCallback
|
||||||
import org.koin.dsl.bind
|
import org.koin.dsl.bind
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
|
||||||
fun Module.configureDIInfrastructure(config: Configuration) {
|
fun Module.configureDIInfrastructure(config: Configuration) {
|
||||||
|
// Postgresql config
|
||||||
single {
|
single {
|
||||||
HikariConfig()
|
HikariConfig()
|
||||||
.apply {
|
.apply {
|
||||||
@@ -30,18 +33,27 @@ fun Module.configureDIInfrastructure(config: Configuration) {
|
|||||||
minimumIdle = 10
|
minimumIdle = 10
|
||||||
}.let {
|
}.let {
|
||||||
HikariDataSource(it)
|
HikariDataSource(it)
|
||||||
|
}.also { datasource ->
|
||||||
|
registerCallback(
|
||||||
|
object : ScopeCallback {
|
||||||
|
override fun onScopeClose(scope: Scope) {
|
||||||
|
datasource.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} bind DataSource::class
|
} bind DataSource::class
|
||||||
|
|
||||||
single {
|
// RabbitMQ config
|
||||||
|
factory {
|
||||||
ConnectionFactory().apply {
|
ConnectionFactory().apply {
|
||||||
host = config.rabbitmq.url
|
host = config.rabbitmq.url
|
||||||
port = config.rabbitmq.port
|
port = config.rabbitmq.port
|
||||||
virtualHost = virtualHost
|
|
||||||
username = config.rabbitmq.username
|
username = config.rabbitmq.username
|
||||||
password = config.rabbitmq.password
|
password = config.rabbitmq.password
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
singleOf(::GameEventBusInRabbinMQ) bind GameEventBus::class
|
singleOf(::GameEventBusInRabbinMQ) bind GameEventBus::class
|
||||||
singleOf(::GameEventStoreInPostgresql) bind GameEventStore::class
|
singleOf(::GameEventStoreInPostgresql) bind GameEventStore::class
|
||||||
singleOf(::GameProjectionBusInMemory) bind GameProjectionBus::class
|
singleOf(::GameProjectionBusInMemory) bind GameProjectionBus::class
|
||||||
|
|||||||
@@ -6,5 +6,9 @@ interface Bus<T> {
|
|||||||
/**
|
/**
|
||||||
* @param priority The higher the priority, the more it will be called first
|
* @param priority The higher the priority, the more it will be called first
|
||||||
*/
|
*/
|
||||||
fun subscribe(block: suspend (T) -> Unit)
|
fun subscribe(block: suspend (T) -> Unit): Subscription
|
||||||
|
|
||||||
|
interface Subscription : AutoCloseable {
|
||||||
|
override fun close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,12 @@ class BusInMemory<E>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribe(block: suspend (E) -> Unit) {
|
override fun subscribe(block: suspend (E) -> Unit): Bus.Subscription {
|
||||||
subscribers.add(block)
|
subscribers.add(block)
|
||||||
|
return object : Bus.Subscription {
|
||||||
|
override fun close() {
|
||||||
|
subscribers.remove(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,90 @@
|
|||||||
package eventDemo.libs.bus
|
package eventDemo.libs.bus
|
||||||
|
|
||||||
import com.rabbitmq.client.CancelCallback
|
import com.rabbitmq.client.AMQP
|
||||||
|
import com.rabbitmq.client.BuiltinExchangeType
|
||||||
|
import com.rabbitmq.client.Connection
|
||||||
import com.rabbitmq.client.ConnectionFactory
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
import com.rabbitmq.client.DeliverCallback
|
import com.rabbitmq.client.DefaultConsumer
|
||||||
import com.rabbitmq.client.Delivery
|
import com.rabbitmq.client.Envelope
|
||||||
import io.ktor.utils.io.core.toByteArray
|
import io.ktor.utils.io.core.toByteArray
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class BusInRabbitMQ<E>(
|
class BusInRabbitMQ<E>(
|
||||||
private val connectionFactory: ConnectionFactory,
|
private val connectionFactory: ConnectionFactory,
|
||||||
private val queueName: String,
|
private val exchangeName: String,
|
||||||
private val objectToString: (E) -> String,
|
private val objectToString: (E) -> String,
|
||||||
private val stringToObject: (String) -> E,
|
private val stringToObject: (String) -> E,
|
||||||
) : Bus<E> {
|
) : Bus<E> {
|
||||||
|
private val connection: Connection = connectionFactory.newConnection()
|
||||||
|
get() {
|
||||||
|
return if (field.isOpen) {
|
||||||
|
field
|
||||||
|
} else {
|
||||||
|
connectionFactory.newConnection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val routingKey = ""
|
||||||
|
|
||||||
init {
|
init {
|
||||||
connectionFactory
|
connection
|
||||||
.newConnection()
|
|
||||||
.createChannel()
|
.createChannel()
|
||||||
.use {
|
.use {
|
||||||
it.queueDeclare(
|
it.exchangeDeclare(
|
||||||
queueName,
|
exchangeName,
|
||||||
|
BuiltinExchangeType.FANOUT,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
emptyMap(),
|
emptyMap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun publish(item: E) {
|
override suspend fun publish(item: E) {
|
||||||
connectionFactory
|
connection
|
||||||
.newConnection()
|
|
||||||
.createChannel()
|
.createChannel()
|
||||||
.use {
|
.use {
|
||||||
it.basicPublish(
|
it.basicPublish(
|
||||||
"",
|
exchangeName,
|
||||||
queueName,
|
routingKey,
|
||||||
null,
|
AMQP.BasicProperties(),
|
||||||
objectToString(item).toByteArray(),
|
objectToString(item).toByteArray(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun subscribe(block: suspend (E) -> Unit) {
|
override fun subscribe(block: suspend (E) -> Unit): Bus.Subscription {
|
||||||
connectionFactory
|
connection
|
||||||
.newConnection()
|
|
||||||
.createChannel()
|
.createChannel()
|
||||||
.basicConsume(
|
.also { channel ->
|
||||||
queueName,
|
val queue =
|
||||||
true,
|
channel
|
||||||
DeliverCallback { _: String, message: Delivery ->
|
.queueDeclare()
|
||||||
runBlocking {
|
.queue
|
||||||
block(stringToObject(message.body.toString(Charsets.UTF_8)))
|
.also { channel.queueBind(it, exchangeName, routingKey) }
|
||||||
|
|
||||||
|
channel
|
||||||
|
.basicConsume(
|
||||||
|
queue,
|
||||||
|
object : DefaultConsumer(channel) {
|
||||||
|
override fun handleDelivery(
|
||||||
|
consumerTag: String,
|
||||||
|
envelope: Envelope,
|
||||||
|
properties: AMQP.BasicProperties,
|
||||||
|
body: ByteArray,
|
||||||
|
) {
|
||||||
|
runBlocking {
|
||||||
|
block(stringToObject(body.toString(Charsets.UTF_8)))
|
||||||
|
}
|
||||||
|
channel.basicAck(envelope.deliveryTag, false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}.let {
|
||||||
|
return object : Bus.Subscription {
|
||||||
|
override fun close() {
|
||||||
|
it.close()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
CancelCallback {},
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eventDemo
|
package eventDemo
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
import eventDemo.business.entity.Card
|
import eventDemo.business.entity.Card
|
||||||
import eventDemo.business.entity.Deck
|
import eventDemo.business.entity.Deck
|
||||||
import eventDemo.configuration.business.configureGameListener
|
import eventDemo.configuration.business.configureGameListener
|
||||||
@@ -26,10 +27,12 @@ fun Deck.allCards(): Set<Card> =
|
|||||||
suspend fun <T> testKoinApplicationWithConfig(block: suspend Koin.() -> T): T =
|
suspend fun <T> testKoinApplicationWithConfig(block: suspend Koin.() -> T): T =
|
||||||
koinApplication { modules(appKoinModule(ApplicationConfig("application.conf").configuration())) }
|
koinApplication { modules(appKoinModule(ApplicationConfig("application.conf").configuration())) }
|
||||||
.koin
|
.koin
|
||||||
.apply {
|
.run {
|
||||||
cleanDataTest()
|
cleanDataTest()
|
||||||
configureGameListener()
|
configureGameListener()
|
||||||
}.block()
|
block()
|
||||||
|
.apply { get<HikariDataSource>().close() }
|
||||||
|
}
|
||||||
|
|
||||||
@KtorDsl
|
@KtorDsl
|
||||||
fun testApplicationWithConfig(
|
fun testApplicationWithConfig(
|
||||||
@@ -53,14 +56,14 @@ fun testApplicationWithConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun DataSource.cleanEventSource() {
|
fun DataSource.cleanEventSource() {
|
||||||
this.connection
|
this.connection.use {
|
||||||
.prepareStatement(
|
it
|
||||||
"""
|
.prepareStatement(
|
||||||
truncate event_stream;
|
"""
|
||||||
""".trimIndent(),
|
truncate event_stream;
|
||||||
).use {
|
""".trimIndent(),
|
||||||
it.execute()
|
).execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun UnifiedJedis.cleanProjections() {
|
fun UnifiedJedis.cleanProjections() {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import eventDemo.business.notification.WelcomeToTheGameNotification
|
|||||||
import eventDemo.libs.event.projection.ProjectionSnapshotRepositoryInMemory
|
import eventDemo.libs.event.projection.ProjectionSnapshotRepositoryInMemory
|
||||||
import eventDemo.testKoinApplicationWithConfig
|
import eventDemo.testKoinApplicationWithConfig
|
||||||
import io.kotest.assertions.nondeterministic.until
|
import io.kotest.assertions.nondeterministic.until
|
||||||
|
import io.kotest.core.NamedTag
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.kotest.matchers.collections.shouldHaveSize
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
import io.kotest.matchers.equals.shouldBeEqual
|
import io.kotest.matchers.equals.shouldBeEqual
|
||||||
@@ -43,6 +44,8 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
@DelicateCoroutinesApi
|
@DelicateCoroutinesApi
|
||||||
class GameSimulationTest :
|
class GameSimulationTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
tags(NamedTag("postgresql"))
|
||||||
|
|
||||||
test("Simulation of a game") {
|
test("Simulation of a game") {
|
||||||
withTimeout(2.seconds) {
|
withTimeout(2.seconds) {
|
||||||
disableShuffleDeck()
|
disableShuffleDeck()
|
||||||
@@ -59,120 +62,120 @@ class GameSimulationTest :
|
|||||||
|
|
||||||
var player1HasJoin = false
|
var player1HasJoin = false
|
||||||
|
|
||||||
val player1Job =
|
|
||||||
launch {
|
|
||||||
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player1)).also { sendCommand ->
|
|
||||||
channelCommand1.send(sendCommand)
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
player1HasJoin = true
|
|
||||||
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<WelcomeToTheGameNotification>(it).players shouldBeEqual setOf(player1)
|
|
||||||
}
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<PlayerAsJoinTheGameNotification>(it).player shouldBeEqual player2
|
|
||||||
}
|
|
||||||
IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(gameId, player1)).also { sendCommand ->
|
|
||||||
channelCommand1.send(sendCommand)
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val player1Hand =
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<TheGameWasStartedNotification>(it).hand shouldHaveSize 7
|
|
||||||
}
|
|
||||||
playedCard1 = player1Hand.first()
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<ItsTheTurnOfNotification>(it).apply {
|
|
||||||
player shouldBeEqual player1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<PlayerWasReadyNotification>(it).player shouldBeEqual player2
|
|
||||||
}
|
|
||||||
|
|
||||||
IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(gameId, player1, player1Hand.first())).also { sendCommand ->
|
|
||||||
channelCommand1.send(sendCommand)
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<ItsTheTurnOfNotification>(it).apply {
|
|
||||||
player shouldBeEqual player2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
channelNotification1.receive().let {
|
|
||||||
assertIs<PlayerAsPlayACardNotification>(it).apply {
|
|
||||||
player shouldBeEqual player2
|
|
||||||
card shouldBeEqual assertNotNull(playedCard2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val player2Job =
|
|
||||||
launch {
|
|
||||||
until(1.seconds) { player1HasJoin }
|
|
||||||
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player2)).also { sendCommand ->
|
|
||||||
channelCommand2.send(sendCommand)
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<WelcomeToTheGameNotification>(it).players shouldBeEqual setOf(player1, player2)
|
|
||||||
}
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<PlayerWasReadyNotification>(it).player shouldBeEqual player1
|
|
||||||
}
|
|
||||||
|
|
||||||
IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(gameId, player2)).also { sendCommand ->
|
|
||||||
channelCommand2.send(sendCommand)
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val player2Hand =
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<TheGameWasStartedNotification>(it).hand shouldHaveSize 7
|
|
||||||
}
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<ItsTheTurnOfNotification>(it).apply {
|
|
||||||
player shouldBeEqual player1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<PlayerAsPlayACardNotification>(it).apply {
|
|
||||||
player shouldBeEqual player1
|
|
||||||
card shouldBeEqual assertNotNull(playedCard1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
playedCard2 = player2Hand.first()
|
|
||||||
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<ItsTheTurnOfNotification>(it).apply {
|
|
||||||
player shouldBeEqual player2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(gameId, player2, player2Hand.first())).also { sendCommand ->
|
|
||||||
channelCommand2.send(sendCommand)
|
|
||||||
channelNotification2.receive().let {
|
|
||||||
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testKoinApplicationWithConfig {
|
testKoinApplicationWithConfig {
|
||||||
|
val player1Job =
|
||||||
|
launch {
|
||||||
|
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player1)).also { sendCommand ->
|
||||||
|
channelCommand1.send(sendCommand)
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
player1HasJoin = true
|
||||||
|
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<WelcomeToTheGameNotification>(it).players shouldBeEqual setOf(player1)
|
||||||
|
}
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<PlayerAsJoinTheGameNotification>(it).player shouldBeEqual player2
|
||||||
|
}
|
||||||
|
IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(gameId, player1)).also { sendCommand ->
|
||||||
|
channelCommand1.send(sendCommand)
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val player1Hand =
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<TheGameWasStartedNotification>(it).hand shouldHaveSize 7
|
||||||
|
}
|
||||||
|
playedCard1 = player1Hand.first()
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<ItsTheTurnOfNotification>(it).apply {
|
||||||
|
player shouldBeEqual player1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<PlayerWasReadyNotification>(it).player shouldBeEqual player2
|
||||||
|
}
|
||||||
|
|
||||||
|
IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(gameId, player1, player1Hand.first())).also { sendCommand ->
|
||||||
|
channelCommand1.send(sendCommand)
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<ItsTheTurnOfNotification>(it).apply {
|
||||||
|
player shouldBeEqual player2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channelNotification1.receive().let {
|
||||||
|
assertIs<PlayerAsPlayACardNotification>(it).apply {
|
||||||
|
player shouldBeEqual player2
|
||||||
|
card shouldBeEqual assertNotNull(playedCard2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val player2Job =
|
||||||
|
launch {
|
||||||
|
until(1.seconds) { player1HasJoin }
|
||||||
|
IWantToJoinTheGameCommand(IWantToJoinTheGameCommand.Payload(gameId, player2)).also { sendCommand ->
|
||||||
|
channelCommand2.send(sendCommand)
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<WelcomeToTheGameNotification>(it).players shouldBeEqual setOf(player1, player2)
|
||||||
|
}
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<PlayerWasReadyNotification>(it).player shouldBeEqual player1
|
||||||
|
}
|
||||||
|
|
||||||
|
IamReadyToPlayCommand(IamReadyToPlayCommand.Payload(gameId, player2)).also { sendCommand ->
|
||||||
|
channelCommand2.send(sendCommand)
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val player2Hand =
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<TheGameWasStartedNotification>(it).hand shouldHaveSize 7
|
||||||
|
}
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<ItsTheTurnOfNotification>(it).apply {
|
||||||
|
player shouldBeEqual player1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<PlayerAsPlayACardNotification>(it).apply {
|
||||||
|
player shouldBeEqual player1
|
||||||
|
card shouldBeEqual assertNotNull(playedCard1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
playedCard2 = player2Hand.first()
|
||||||
|
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<ItsTheTurnOfNotification>(it).apply {
|
||||||
|
player shouldBeEqual player2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IWantToPlayCardCommand(IWantToPlayCardCommand.Payload(gameId, player2, player2Hand.first())).also { sendCommand ->
|
||||||
|
channelCommand2.send(sendCommand)
|
||||||
|
channelNotification2.receive().let {
|
||||||
|
assertIs<CommandSuccessNotification>(it).commandId shouldBeEqual sendCommand.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val commandHandler by inject<GameCommandHandler>()
|
val commandHandler by inject<GameCommandHandler>()
|
||||||
val eventStore by inject<GameEventStore>()
|
val eventStore by inject<GameEventStore>()
|
||||||
val playerNotificationListener by inject<PlayerNotificationListener>()
|
val playerNotificationListener by inject<PlayerNotificationListener>()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import eventDemo.business.notification.CommandSuccessNotification
|
|||||||
import eventDemo.business.notification.Notification
|
import eventDemo.business.notification.Notification
|
||||||
import eventDemo.business.notification.WelcomeToTheGameNotification
|
import eventDemo.business.notification.WelcomeToTheGameNotification
|
||||||
import eventDemo.testKoinApplicationWithConfig
|
import eventDemo.testKoinApplicationWithConfig
|
||||||
|
import io.kotest.core.NamedTag
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.kotest.matchers.collections.shouldContain
|
import io.kotest.matchers.collections.shouldContain
|
||||||
import io.kotest.matchers.equals.shouldBeEqual
|
import io.kotest.matchers.equals.shouldBeEqual
|
||||||
@@ -24,6 +25,8 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class GameCommandHandlerTest :
|
class GameCommandHandlerTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
tags(NamedTag("postgresql"))
|
||||||
|
|
||||||
test("handle a command should execute the command") {
|
test("handle a command should execute the command") {
|
||||||
withTimeout(5.seconds) {
|
withTimeout(5.seconds) {
|
||||||
testKoinApplicationWithConfig {
|
testKoinApplicationWithConfig {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import eventDemo.business.event.projection.gameState.GameStateRepository
|
|||||||
import eventDemo.testKoinApplicationWithConfig
|
import eventDemo.testKoinApplicationWithConfig
|
||||||
import io.kotest.assertions.nondeterministic.eventually
|
import io.kotest.assertions.nondeterministic.eventually
|
||||||
import io.kotest.assertions.nondeterministic.eventuallyConfig
|
import io.kotest.assertions.nondeterministic.eventuallyConfig
|
||||||
|
import io.kotest.core.NamedTag
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.kotest.matchers.collections.shouldHaveSize
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
import io.kotest.matchers.equals.shouldBeEqual
|
import io.kotest.matchers.equals.shouldBeEqual
|
||||||
@@ -23,6 +24,8 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
class GameStateRepositoryTest :
|
class GameStateRepositoryTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
tags(NamedTag("postgresql"))
|
||||||
|
|
||||||
val player1 = Player("Tesla")
|
val player1 = Player("Tesla")
|
||||||
val player2 = Player(name = "Einstein")
|
val player2 = Player(name = "Einstein")
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package eventDemo.adapter.infrastructureLayer
|
package eventDemo.externalServices
|
||||||
|
|
||||||
import eventDemo.testKoinApplicationWithConfig
|
import eventDemo.testKoinApplicationWithConfig
|
||||||
import io.kotest.core.NamedTag
|
import io.kotest.core.NamedTag
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eventDemo.adapter.infrastructureLayer
|
package eventDemo.externalServices
|
||||||
|
|
||||||
import io.kotest.core.NamedTag
|
import io.kotest.core.NamedTag
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
package eventDemo.libs.bus
|
package eventDemo.libs.bus
|
||||||
|
|
||||||
import com.rabbitmq.client.ConnectionFactory
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
import io.kotest.assertions.nondeterministic.until
|
import io.kotest.assertions.nondeterministic.eventually
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.kotest.datatest.withData
|
import io.kotest.datatest.withData
|
||||||
import io.kotest.matchers.equals.shouldBeEqual
|
import io.kotest.matchers.string.shouldStartWith
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.spyk
|
||||||
|
import io.mockk.verify
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -19,7 +22,6 @@ class BusTest :
|
|||||||
ConnectionFactory().apply {
|
ConnectionFactory().apply {
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
port = 5672
|
port = 5672
|
||||||
virtualHost = virtualHost
|
|
||||||
username = "event-demo"
|
username = "event-demo"
|
||||||
password = "changeit"
|
password = "changeit"
|
||||||
}
|
}
|
||||||
@@ -29,24 +31,24 @@ class BusTest :
|
|||||||
BusInRabbitMQ::class.java.simpleName to
|
BusInRabbitMQ::class.java.simpleName to
|
||||||
BusInRabbitMQ(
|
BusInRabbitMQ(
|
||||||
factory,
|
factory,
|
||||||
"testQueue",
|
"testExchange",
|
||||||
{ it.value },
|
{ it.value },
|
||||||
{ ObjTest(it) },
|
{ ObjTest(it) },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
withData(list) { bus ->
|
withData(list) { bus ->
|
||||||
val value = "hello${Random.nextInt()}"
|
val spy = spyk(mockk<() -> Unit>())
|
||||||
var isCalled = false
|
|
||||||
|
|
||||||
bus.subscribe { obj ->
|
bus.subscribe { obj ->
|
||||||
isCalled = true
|
spy()
|
||||||
obj.value shouldBeEqual value
|
obj.value shouldStartWith "testMessage"
|
||||||
}
|
}
|
||||||
bus.publish(ObjTest(value))
|
bus.publish(ObjTest("testMessage${Random.nextInt()}"))
|
||||||
|
bus.publish(ObjTest("testMessage${Random.nextInt()}"))
|
||||||
|
|
||||||
until(3.seconds) {
|
eventually(1.seconds) {
|
||||||
isCalled shouldBeEqual true
|
verify(exactly = 2) { spy() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eventDemo.libs.event
|
package eventDemo.libs.event
|
||||||
|
|
||||||
import eventDemo.testKoinApplicationWithConfig
|
import eventDemo.testKoinApplicationWithConfig
|
||||||
|
import io.kotest.core.NamedTag
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.kotest.datatest.withData
|
import io.kotest.datatest.withData
|
||||||
import io.kotest.matchers.collections.shouldHaveSize
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
@@ -12,11 +13,14 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.junit.jupiter.api.assertNull
|
import org.junit.jupiter.api.assertNull
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
import org.koin.core.Koin
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
@DelicateCoroutinesApi
|
@DelicateCoroutinesApi
|
||||||
class EventStreamTest :
|
class EventStreamTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
|
tags(NamedTag("postgresql"))
|
||||||
|
|
||||||
fun EventStream<EventXTest, IdTest>.with3Events(block: EventStream<EventXTest, IdTest>.(id: IdTest) -> Unit) =
|
fun EventStream<EventXTest, IdTest>.with3Events(block: EventStream<EventXTest, IdTest>.(id: IdTest) -> Unit) =
|
||||||
also {
|
also {
|
||||||
publish(EventXTest(aggregateId = aggregateId, version = 1, num = 1))
|
publish(EventXTest(aggregateId = aggregateId, version = 1, num = 1))
|
||||||
@@ -25,37 +29,39 @@ class EventStreamTest :
|
|||||||
block(aggregateId)
|
block(aggregateId)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun eventStreams(): List<EventStream<EventXTest, IdTest>> =
|
fun Koin.eventStreams(): List<EventStream<EventXTest, IdTest>> =
|
||||||
testKoinApplicationWithConfig {
|
listOf(
|
||||||
listOf(
|
EventStreamInMemory(IdTest()),
|
||||||
EventStreamInMemory(IdTest()),
|
EventStreamInPostgresql(
|
||||||
EventStreamInPostgresql(
|
IdTest(),
|
||||||
IdTest(),
|
dataSource = get(),
|
||||||
dataSource = get(),
|
objectToString = { Json.encodeToString(it) },
|
||||||
objectToString = { Json.encodeToString(it) },
|
stringToObject = { Json.decodeFromString(it) },
|
||||||
stringToObject = { Json.decodeFromString(it) },
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
context("readVersionBetween should only return the event of aggregate") {
|
context("readVersionBetween should only return the event of aggregate") {
|
||||||
withData(eventStreams()) { stream ->
|
testKoinApplicationWithConfig {
|
||||||
stream.with3Events {
|
withData(eventStreams()) { stream ->
|
||||||
readVersionBetween(1..2) shouldHaveSize 2
|
stream.with3Events {
|
||||||
readVersionBetween(1..1) shouldHaveSize 1
|
readVersionBetween(1..2) shouldHaveSize 2
|
||||||
readVersionBetween(2..20) shouldHaveSize 2
|
readVersionBetween(1..1) shouldHaveSize 1
|
||||||
readVersionBetween(4..20) shouldHaveSize 0
|
readVersionBetween(2..20) shouldHaveSize 2
|
||||||
|
readVersionBetween(4..20) shouldHaveSize 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context("readAll should only return the event of aggregate") {
|
context("readAll should only return the event of aggregate") {
|
||||||
withData(eventStreams()) { stream ->
|
testKoinApplicationWithConfig {
|
||||||
stream.with3Events {
|
withData(eventStreams()) { stream ->
|
||||||
readAll() shouldHaveSize 3
|
stream.with3Events {
|
||||||
readAll().also {
|
readAll() shouldHaveSize 3
|
||||||
it.forEachIndexed { i, event ->
|
readAll().also {
|
||||||
event.version shouldBeEqual i + 1
|
it.forEachIndexed { i, event ->
|
||||||
|
event.version shouldBeEqual i + 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,50 +69,58 @@ class EventStreamTest :
|
|||||||
}
|
}
|
||||||
|
|
||||||
context("getByVersion should only return the event with this version") {
|
context("getByVersion should only return the event with this version") {
|
||||||
withData(eventStreams()) { stream ->
|
testKoinApplicationWithConfig {
|
||||||
stream.with3Events {
|
withData(eventStreams()) { stream ->
|
||||||
assertNotNull(getByVersion(1)).version shouldBeEqual 1
|
stream.with3Events {
|
||||||
assertNotNull(getByVersion(2)).version shouldBeEqual 2
|
assertNotNull(getByVersion(1)).version shouldBeEqual 1
|
||||||
assertNotNull(getByVersion(3)).version shouldBeEqual 3
|
assertNotNull(getByVersion(2)).version shouldBeEqual 2
|
||||||
assertNull(getByVersion(4))
|
assertNotNull(getByVersion(3)).version shouldBeEqual 3
|
||||||
|
assertNull(getByVersion(4))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context("readGreaterOfVersion should only return the events with greater version") {
|
context("readGreaterOfVersion should only return the events with greater version") {
|
||||||
withData(eventStreams()) {
|
testKoinApplicationWithConfig {
|
||||||
it.with3Events {
|
withData(eventStreams()) {
|
||||||
assertNotNull(readGreaterOfVersion(1)) shouldHaveSize 2
|
it.with3Events {
|
||||||
assertNotNull(readGreaterOfVersion(2)) shouldHaveSize 1
|
assertNotNull(readGreaterOfVersion(1)) shouldHaveSize 2
|
||||||
assertNotNull(readGreaterOfVersion(3)) shouldHaveSize 0
|
assertNotNull(readGreaterOfVersion(2)) shouldHaveSize 1
|
||||||
assertNotNull(readGreaterOfVersion(30)) shouldHaveSize 0
|
assertNotNull(readGreaterOfVersion(3)) shouldHaveSize 0
|
||||||
|
assertNotNull(readGreaterOfVersion(30)) shouldHaveSize 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context("publish should be throw error when publish another aggregate event") {
|
context("publish should be throw error when publish another aggregate event") {
|
||||||
withData(eventStreams()) {
|
testKoinApplicationWithConfig {
|
||||||
assertThrows<EventStreamPublishException> { it.publish(EventXTest(aggregateId = IdTest(), version = 1, num = 1)) }
|
withData(eventStreams()) {
|
||||||
|
assertThrows<EventStreamPublishException> { it.publish(EventXTest(aggregateId = IdTest(), version = 1, num = 1)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context("publish should be concurrently secure") {
|
context("publish should be concurrently secure") {
|
||||||
withData(eventStreams()) { stream ->
|
testKoinApplicationWithConfig {
|
||||||
(0..9)
|
withData(eventStreams()) { stream ->
|
||||||
.map { i1 ->
|
(0..9)
|
||||||
GlobalScope.launch {
|
.map { i1 ->
|
||||||
(1..10).forEach { i2 ->
|
GlobalScope.launch {
|
||||||
stream.publish(
|
(1..10).forEach { i2 ->
|
||||||
EventXTest(
|
stream.publish(
|
||||||
aggregateId = stream.aggregateId,
|
EventXTest(
|
||||||
version = (i1 * 10) + i2,
|
aggregateId = stream.aggregateId,
|
||||||
num = (i1 * 10) + i2,
|
version = (i1 * 10) + i2,
|
||||||
),
|
num = (i1 * 10) + i2,
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}.joinAll()
|
||||||
}.joinAll()
|
stream.readAll() shouldHaveSize 100
|
||||||
stream.readAll() shouldHaveSize 100
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user