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