Compare commits
5
Commits
v0.2
...
da13b0d2b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da13b0d2b8
|
||
|
|
d632dc0f6b
|
||
|
|
b60e4aa457
|
||
|
|
e262f35b27
|
||
|
|
77e4cab8ad
|
@@ -0,0 +1,26 @@
|
||||
# Version control
|
||||
.git
|
||||
.github
|
||||
|
||||
# Gradle build outputs / caches (must always be rebuilt fresh inside the image)
|
||||
.gradle
|
||||
build/
|
||||
.kotlin
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
.run
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# Docker-only local files (secrets/env must never be baked into the image)
|
||||
docker/.env
|
||||
docker/*.env.docker
|
||||
docker/*.secret
|
||||
|
||||
# Misc
|
||||
*.hprof
|
||||
.gradle-docker-cache/
|
||||
@@ -0,0 +1,7 @@
|
||||
* text=auto
|
||||
* eol=lf
|
||||
*.sh text eol=lf
|
||||
*.png binary
|
||||
*.jar binary
|
||||
gradlew.bat eol=crlf
|
||||
gradlew text eol=lf
|
||||
+30
-18
@@ -80,36 +80,48 @@ jobs:
|
||||
continue-on-error: false
|
||||
|
||||
test:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GRADLE_CACHE_DIR: ${{ github.workspace }}/.gradle-docker-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
- name: Install a pinned Docker Compose version
|
||||
run: |
|
||||
mkdir -p ~/.docker/cli-plugins
|
||||
curl -fSL https://github.com/docker/compose/releases/download/v5.1.4/docker-compose-linux-x86_64 \
|
||||
-o ~/.docker/cli-plugins/docker-compose
|
||||
chmod +x ~/.docker/cli-plugins/docker-compose
|
||||
docker compose version
|
||||
|
||||
- name: Restore Gradle cache
|
||||
- name: Prepare docker secrets
|
||||
run: |
|
||||
[ -f docker/postgresql.secret ] || echo -n "changeit" > docker/postgresql.secret
|
||||
|
||||
- name: Generate cache key
|
||||
id: cache-key-generator
|
||||
run: echo "key=gradle-docker-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Restore Gradle cache (Docker)
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ needs.build.outputs.cache-key }}
|
||||
path: ${{ env.GRADLE_CACHE_DIR }}
|
||||
key: ${{ steps.cache-key-generator.outputs.key }}
|
||||
restore-keys: |
|
||||
gradle-${{ runner.os }}-
|
||||
gradle-docker-${{ runner.os }}-
|
||||
|
||||
- name: Grant execute permission to Gradle wrapper
|
||||
run: chmod +x gradlew
|
||||
- name: Prepare cache directory permissions
|
||||
run: |
|
||||
mkdir -p "$GRADLE_CACHE_DIR"
|
||||
chmod -R 777 "$GRADLE_CACHE_DIR"
|
||||
|
||||
- name: Start CI Docker Compose services
|
||||
run: ./gradlew ciComposeUp -Pci
|
||||
- name: Run tests in Docker
|
||||
run: docker compose -f docker/docker-compose-test.yaml run tests
|
||||
|
||||
- name: Run tests
|
||||
run: ./gradlew test -x ciComposeUp -Pci --no-daemon
|
||||
- name: Shut down Docker services
|
||||
if: always()
|
||||
run: docker compose -f docker/docker-compose-test.yaml down -v
|
||||
|
||||
- name: Upload test reports
|
||||
if: always()
|
||||
|
||||
@@ -37,3 +37,4 @@ out/
|
||||
/docker/.env
|
||||
/docker/*.secret
|
||||
*.hprof
|
||||
/.gradle-docker-cache/
|
||||
|
||||
+11
-71
@@ -1,4 +1,3 @@
|
||||
import com.avast.gradle.dockercompose.ComposeExtension
|
||||
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
||||
|
||||
val ktorVersion: Provider<String> = providers.gradleProperty("ktor_version")
|
||||
@@ -15,7 +14,6 @@ plugins {
|
||||
id("io.ktor.plugin") version "3.5.1"
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.4.10"
|
||||
id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
|
||||
id("com.avast.gradle.docker-compose") version "0.17.12"
|
||||
}
|
||||
|
||||
group = "io.github.flecomte"
|
||||
@@ -48,74 +46,16 @@ java {
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
configure<ComposeExtension> {
|
||||
createNested("test").apply {
|
||||
useComposeFiles.set(listOf("docker/docker-compose-test.yaml"))
|
||||
setProjectName("event-demo-test")
|
||||
}
|
||||
createNested("dev").apply {
|
||||
useComposeFiles.set(listOf("docker/docker-compose-dev.yaml"))
|
||||
setProjectName("event-demo-dev")
|
||||
}
|
||||
createNested("ci").apply {
|
||||
useComposeFiles.set(listOf("docker/docker-compose-ci.yaml"))
|
||||
setProjectName("event-demo-ci")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
if (project.hasProperty("ci")) {
|
||||
dependsOn("ciComposeUp")
|
||||
} else {
|
||||
dependsOn("testComposeUp")
|
||||
}
|
||||
}
|
||||
tasks.named("run") {
|
||||
dependsOn("devComposeUp")
|
||||
}
|
||||
tasks.composeUp {
|
||||
dependsOn("copyEnv")
|
||||
}
|
||||
tasks.named("devComposeUp") {
|
||||
dependsOn("copyEnv")
|
||||
}
|
||||
tasks.named("testComposeUp") {
|
||||
dependsOn("copyEnv")
|
||||
}
|
||||
tasks.named("ciComposeUp") {
|
||||
dependsOn("copyEnv")
|
||||
}
|
||||
|
||||
tasks.register<Copy>("copyEnv") {
|
||||
group = "docker"
|
||||
description = "copy the default dotenv file"
|
||||
from("docker")
|
||||
into("docker")
|
||||
rename {
|
||||
it.removeSuffix(".template")
|
||||
}
|
||||
include(".env.template")
|
||||
eachFile {
|
||||
if (File("docker/$name").exists()) {
|
||||
exclude()
|
||||
}
|
||||
}
|
||||
val files =
|
||||
buildList {
|
||||
add(File("docker/postgresql.secret"))
|
||||
if (!project.hasProperty("ci")) {
|
||||
add(File("docker/pgadmin.secret"))
|
||||
}
|
||||
}
|
||||
outputs.files(*files.toTypedArray())
|
||||
doLast {
|
||||
files.forEach {
|
||||
if (!it.exists()) {
|
||||
it.writeText("changeit")
|
||||
}
|
||||
}
|
||||
jvmArgs("-Djdk.attach.allowAttachSelf=true", "-XX:+EnableDynamicAgentLoading")
|
||||
// Dynamic self-attach (used by MockK/ByteBuddy) times out in Docker containers because the
|
||||
// SIGQUIT-triggered AttachListener handshake never completes there. Loading the byte-buddy
|
||||
// agent jar statically via -javaagent avoids the attach handshake entirely: MockK detects the
|
||||
// pre-installed Instrumentation instance and skips dynamic attach.
|
||||
doFirst {
|
||||
val agentJar =
|
||||
classpath.files.firstOrNull { it.name.startsWith("byte-buddy-agent") }
|
||||
?: error("byte-buddy-agent jar not found on test classpath")
|
||||
jvmArgs("-javaagent:$agentJar")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +94,6 @@ dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion.get()}")
|
||||
testImplementation("io.ktor:ktor-server-test-host-jvm:${ktorVersion.get()}")
|
||||
testImplementation("io.kotest:kotest-runner-junit5:${kotestVersion.get()}")
|
||||
testImplementation("io.mockk:mockk:1.13.17")
|
||||
testImplementation("io.mockk:mockk:1.14.11")
|
||||
testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0")
|
||||
}
|
||||
|
||||
+13
-1
@@ -1,12 +1,24 @@
|
||||
Installation
|
||||
============
|
||||
|
||||
To run the stack:
|
||||
To run the stack in production:
|
||||
|
||||
```shell
|
||||
docker compose -f docker\docker-compose-prod.yaml -p event-demo up -d
|
||||
```
|
||||
|
||||
To run only the app dependencies in development mode and run the app localy (not in docker):
|
||||
|
||||
```shell
|
||||
docker compose -f docker\docker-compose-dev.yaml up -d
|
||||
```
|
||||
|
||||
To run the tests in docker (it's designed for the CI):
|
||||
|
||||
```shell
|
||||
docker compose -f docker\docker-compose-test.yaml up -d
|
||||
```
|
||||
|
||||
Api url:
|
||||
- [Backend API](http://api.traefik.me/)
|
||||
- [Frontend web site](http://app.traefik.me/) (WIP)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
REDIS_URL=redis://redis:6379
|
||||
POSTGRESQL_URL=jdbc:postgresql://postgresql/event-demo
|
||||
RABBITMQ_URL=rabbitmq
|
||||
@@ -1 +0,0 @@
|
||||
PGADMIN_DEFAULT_EMAIL=
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
# Stage 1: Cache Gradle dependencies
|
||||
FROM gradle:latest AS cache
|
||||
FROM gradle:9.6.1-jdk21-alpine AS cache
|
||||
RUN mkdir -p /home/gradle/cache_home
|
||||
ENV GRADLE_USER_HOME=/home/gradle/cache_home
|
||||
COPY build.gradle.* gradle.properties /home/gradle/app/
|
||||
@@ -7,7 +7,7 @@ WORKDIR /home/gradle/app
|
||||
RUN gradle build -i -x check
|
||||
|
||||
# Stage 2: Build Application
|
||||
FROM gradle:latest AS build
|
||||
FROM gradle:9.6.1-jdk21-alpine AS build
|
||||
COPY --from=cache /home/gradle/cache_home /home/gradle/.gradle
|
||||
COPY --chown=gradle:gradle . /home/gradle/src
|
||||
WORKDIR /home/gradle/src
|
||||
@@ -16,8 +16,8 @@ WORKDIR /home/gradle/src
|
||||
RUN gradle buildFatJar --no-daemon
|
||||
|
||||
# Stage 3: Create the Runtime Image
|
||||
FROM amazoncorretto:21 AS runtime
|
||||
FROM eclipse-temurin:21-jre-alpine AS runtime
|
||||
EXPOSE 8080
|
||||
RUN mkdir /app
|
||||
COPY --from=build /home/gradle/src/build/libs/*.jar /app/event-demo-all.jar
|
||||
COPY --from=build /home/gradle/src/build/libs/*-all.jar /app/event-demo-all.jar
|
||||
ENTRYPOINT ["java","-jar","/app/event-demo-all.jar"]
|
||||
@@ -0,0 +1,10 @@
|
||||
# Image officielle Gradle avec JDK 21 déjà installé
|
||||
FROM gradle:9.6.1-jdk21
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copie du wrapper et des fichiers de config en premier pour profiter du cache Docker
|
||||
COPY build.gradle.kts settings.gradle.kts ./
|
||||
|
||||
# Lance les tests Kotlin
|
||||
CMD ["gradle", "test", "--no-daemon"]
|
||||
@@ -1,5 +0,0 @@
|
||||
name: event-demo-ci
|
||||
include:
|
||||
- path:
|
||||
- parts/docker-compose-databases.yaml
|
||||
- parts/docker-compose-databases-expose.yaml
|
||||
@@ -1,4 +1,17 @@
|
||||
name: event-demo-test
|
||||
name: event-demo-dev
|
||||
include:
|
||||
- path:
|
||||
- docker-compose-test.yaml
|
||||
- parts/docker-compose-databases.yaml
|
||||
- parts/docker-compose-databases-expose.yaml
|
||||
- parts/docker-compose-tools.yaml
|
||||
- parts/docker-compose-tools-local.yaml
|
||||
- parts/docker-compose-traefik.yaml
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "changeit"
|
||||
|
||||
pgadmin:
|
||||
environment:
|
||||
PGADMIN_DEFAULT_PASSWORD: "changeit"
|
||||
@@ -5,3 +5,16 @@ include:
|
||||
- parts/docker-compose-app.yaml
|
||||
- parts/docker-compose-tools.yaml
|
||||
- parts/docker-compose-traefik.yaml
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
environment:
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/postgresql_password
|
||||
volumes:
|
||||
- ./postgresql.secret:/run/secrets/postgresql_password:ro
|
||||
|
||||
pgadmin:
|
||||
environment:
|
||||
PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/pgadmin_password
|
||||
volumes:
|
||||
- ./pgadmin.secret:/run/secrets/pgadmin_password:ro
|
||||
@@ -2,6 +2,10 @@ name: event-demo-test
|
||||
include:
|
||||
- path:
|
||||
- parts/docker-compose-databases.yaml
|
||||
- parts/docker-compose-databases-expose.yaml
|
||||
- parts/docker-compose-tools.yaml
|
||||
- parts/docker-compose-traefik.yaml
|
||||
- parts/docker-compose-test.yaml
|
||||
- parts/docker-compose-traefik.yaml
|
||||
|
||||
services:
|
||||
postgresql:
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "changeit"
|
||||
@@ -12,6 +12,8 @@ services:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- ../.env.docker
|
||||
labels:
|
||||
- "traefik.http.routers.api.rule=Host(`api.traefik.me`)"
|
||||
- "traefik.http.services.api.loadbalancer.server.port=8080"
|
||||
|
||||
@@ -22,7 +22,6 @@ services:
|
||||
image: postgres:18.4
|
||||
command: postgres -c 'max_connections=500'
|
||||
environment:
|
||||
POSTGRES_PASSWORD_FILE: /run/secrets/postgresql_password
|
||||
POSTGRES_USER: event-demo
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "sh -c 'pg_isready -U event-demo'"]
|
||||
@@ -31,7 +30,6 @@ services:
|
||||
retries: 10
|
||||
volumes:
|
||||
- postgresql_data:/var/lib/postgresql
|
||||
- ../postgresql.secret:/run/secrets/postgresql_password:ro
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:4-management-alpine
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
tests:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: docker/DockerfileTest
|
||||
volumes:
|
||||
- ${GRADLE_CACHE_DIR:-gradle-cache}:/home/gradle/.gradle
|
||||
- ../..:/app
|
||||
depends_on:
|
||||
flyway:
|
||||
condition: service_completed_successfully
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- ../.env.docker
|
||||
|
||||
volumes:
|
||||
gradle-cache:
|
||||
@@ -0,0 +1,18 @@
|
||||
services:
|
||||
pgadmin:
|
||||
environment:
|
||||
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
||||
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
|
||||
configs:
|
||||
- source: pgpass
|
||||
target: /pgpass
|
||||
mode: 0600
|
||||
uid: "5050"
|
||||
gid: "5050"
|
||||
- source: servers_json
|
||||
target: /pgadmin4/servers.json
|
||||
|
||||
configs:
|
||||
pgpass:
|
||||
content: |
|
||||
*:*:*:event-demo:changeit
|
||||
@@ -2,20 +2,10 @@ services:
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: $PGADMIN_DEFAULT_EMAIL
|
||||
PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/pgadmin_password
|
||||
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
||||
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: 'False'
|
||||
secrets:
|
||||
- pgadmin_password
|
||||
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@event-demo.dev}
|
||||
volumes:
|
||||
- pgadmin_data:/var/lib/pgadmin
|
||||
configs:
|
||||
- source: pgpass
|
||||
target: /pgpass
|
||||
mode: 0600
|
||||
uid: "5050"
|
||||
gid: "5050"
|
||||
- source: servers_json
|
||||
target: /pgadmin4/servers.json
|
||||
labels:
|
||||
@@ -35,9 +25,6 @@ services:
|
||||
- "traefik.http.services.rabbitmq-management.loadbalancer.server.port=15672"
|
||||
|
||||
configs:
|
||||
pgpass:
|
||||
content: |
|
||||
*:*:*:event-demo:changeit
|
||||
servers_json:
|
||||
content: |
|
||||
{
|
||||
@@ -55,9 +42,5 @@ configs:
|
||||
}
|
||||
}
|
||||
|
||||
secrets:
|
||||
pgadmin_password:
|
||||
file: ../pgadmin.secret
|
||||
|
||||
volumes:
|
||||
pgadmin_data:
|
||||
@@ -26,11 +26,13 @@ import eventDemo.testHelpers.createNewUser
|
||||
import eventDemo.testHelpers.testKoinApplicationWithConfig
|
||||
import io.kotest.assertions.nondeterministic.eventually
|
||||
import io.kotest.assertions.nondeterministic.until
|
||||
import io.kotest.assertions.retry
|
||||
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.should
|
||||
import io.kotest.matchers.types.shouldBeInstanceOf
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -49,223 +51,225 @@ class GameSimulationTest :
|
||||
tags(Tag.Postgresql)
|
||||
|
||||
test("Simulation of a game") {
|
||||
withTimeout(10.seconds) {
|
||||
disableRandomForTest()
|
||||
val gameId = GameId()
|
||||
val user1 = createNewUser("user1")
|
||||
val user2 = createNewUser("user2")
|
||||
should {
|
||||
retry(maxRetry = 3, timeout = 20.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)
|
||||
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 playedCard1: Card? = null
|
||||
var playedCard2: Card? = null
|
||||
|
||||
var player1HasJoin = false
|
||||
var player1HasJoin = false
|
||||
|
||||
testKoinApplicationWithConfig {
|
||||
val gameRepository = get<GameRepository>()
|
||||
val userRepository = get<UserRepository>()
|
||||
userRepository.run {
|
||||
save(user1)
|
||||
save(user2)
|
||||
}
|
||||
testKoinApplicationWithConfig {
|
||||
val gameRepository = get<GameRepository>()
|
||||
val userRepository = get<UserRepository>()
|
||||
userRepository.run {
|
||||
save(user1)
|
||||
save(user2)
|
||||
}
|
||||
|
||||
gameRepository.create(gameId)
|
||||
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,
|
||||
)
|
||||
}
|
||||
// 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)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch {
|
||||
for (notification in channelNotification2) {
|
||||
player2Notifications.add(notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Player 1 actions
|
||||
val player1Job =
|
||||
launch {
|
||||
createGameWithCommandsInChannels(channelCommand1, gameId, user1) {
|
||||
|
||||
// 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
|
||||
joinTheGame()
|
||||
player1Notifications.waitNotification<WelcomeToTheGameNotification> {
|
||||
players.map { it.userId }.contains(user1.id)
|
||||
}
|
||||
|
||||
playCard(playedCard1!!)
|
||||
player1HasJoin = true
|
||||
|
||||
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
|
||||
player1Notifications.waitNotification<PlayerAsJoinTheGameNotification> {
|
||||
player.userId == user2.id
|
||||
}
|
||||
|
||||
playCard(playedCard2)
|
||||
readyToPlay()
|
||||
player1Notifications.waitNotification<PlayerWasReadyNotification> {
|
||||
playerId == getPlayer(user2).id
|
||||
}
|
||||
|
||||
player2Notifications.waitNotification<ItsTheTurnOfNotification> {
|
||||
player.userId == user1.id
|
||||
}
|
||||
player2Notifications.waitNotification<PlayerAsPlayACardNotification> {
|
||||
playerId == currentPlayer.id && card == playedCard2
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait the end of the game
|
||||
joinAll(player1Job, player2Job)
|
||||
// Player 2 actions
|
||||
val player2Job =
|
||||
launch {
|
||||
createGameWithCommandsInChannels(channelCommand2, gameId, user2) {
|
||||
// wait player 1 has joined the game
|
||||
until(3.seconds) { player1HasJoin }
|
||||
|
||||
// Build the last state from the event store
|
||||
val game = gameRepository.get(gameId)
|
||||
assertInstanceOf<GameStarted>(game)
|
||||
joinTheGame()
|
||||
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,7 +278,7 @@ class GameSimulationTest :
|
||||
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) {
|
||||
return eventually(5.seconds) {
|
||||
filterIsInstance<T>()
|
||||
.first { block(it) }
|
||||
.also { remove(it) }
|
||||
|
||||
@@ -2,6 +2,7 @@ package eventDemo.libs.bus
|
||||
|
||||
import com.rabbitmq.client.ConnectionFactory
|
||||
import eventDemo.testHelpers.spyPing
|
||||
import eventDemo.testHelpers.testKoinApplicationWithConfig
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.datatest.withData
|
||||
import io.kotest.matchers.string.shouldStartWith
|
||||
@@ -15,33 +16,28 @@ private data class ObjTest(
|
||||
class BusTest :
|
||||
FunSpec({
|
||||
context("Pub/sub") {
|
||||
val factory =
|
||||
ConnectionFactory().apply {
|
||||
host = "localhost"
|
||||
port = 5672
|
||||
username = "event-demo"
|
||||
password = "changeit"
|
||||
}
|
||||
val list: Map<String, Bus<ObjTest>> =
|
||||
mapOf(
|
||||
BusInMemory::class.java.simpleName to BusInMemory(),
|
||||
BusInRabbitMQ::class.java.simpleName to
|
||||
BusInRabbitMQ(
|
||||
factory,
|
||||
"testExchange",
|
||||
{ it.value },
|
||||
{ ObjTest(it) },
|
||||
),
|
||||
)
|
||||
testKoinApplicationWithConfig {
|
||||
val list: Map<String, Bus<ObjTest>> =
|
||||
mapOf(
|
||||
BusInMemory::class.java.simpleName to BusInMemory(),
|
||||
BusInRabbitMQ::class.java.simpleName to
|
||||
BusInRabbitMQ(
|
||||
get<ConnectionFactory>(),
|
||||
"testExchange",
|
||||
{ it.value },
|
||||
{ ObjTest(it) },
|
||||
),
|
||||
)
|
||||
|
||||
withData(list) { bus ->
|
||||
spyPing(exactly = 2, duration = 1.seconds) { ping ->
|
||||
bus.subscribe { obj ->
|
||||
ping()
|
||||
obj.value shouldStartWith "testMessage"
|
||||
withData(list) { bus ->
|
||||
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()}"))
|
||||
}
|
||||
bus.publish(ObjTest("testMessage${Random.nextInt()}"))
|
||||
bus.publish(ObjTest("testMessage${Random.nextInt()}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,13 @@ import org.koin.core.module.KoinApplicationDslMarker
|
||||
import org.koin.dsl.koinApplication
|
||||
import org.koin.ktor.ext.getKoin
|
||||
|
||||
const val CONFIG_FILE_NAME = "application.conf"
|
||||
|
||||
@KoinApplicationDslMarker
|
||||
suspend fun <T> testKoinApplicationWithConfig(block: suspend Koin.() -> T): T =
|
||||
koinApplication { modules(appKoinModule(ApplicationConfig("application.conf").configuration)) }
|
||||
.koin
|
||||
koinApplication {
|
||||
modules(appKoinModule(ApplicationConfig(CONFIG_FILE_NAME).configuration))
|
||||
}.koin
|
||||
.run {
|
||||
cleanDataTest()
|
||||
configureProjectionListener()
|
||||
@@ -34,7 +37,7 @@ fun testApplicationWithConfig(
|
||||
) {
|
||||
val logger = KotlinLogging.logger {}
|
||||
testApplication {
|
||||
val conf = ApplicationConfig("application.conf")
|
||||
val conf = ApplicationConfig(CONFIG_FILE_NAME)
|
||||
environment {
|
||||
config = conf
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user