chore: refactor docker compose env's
This commit is contained in:
@@ -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/
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
chmod -R 777 "$GRADLE_CACHE_DIR"
|
chmod -R 777 "$GRADLE_CACHE_DIR"
|
||||||
|
|
||||||
- name: Run tests in Docker
|
- name: Run tests in Docker
|
||||||
run: docker compose -f docker/docker-compose-test.yaml run --rm tests
|
run: docker compose -f docker/docker-compose-test.yaml run tests
|
||||||
|
|
||||||
- name: Shut down Docker services
|
- name: Shut down Docker services
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
@@ -58,40 +58,6 @@ tasks.withType<Test>().configureEach {
|
|||||||
jvmArgs("-javaagent:$agentJar")
|
jvmArgs("-javaagent:$agentJar")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tasks.test {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("io.ktor:ktor-server-core-jvm")
|
implementation("io.ktor:ktor-server-core-jvm")
|
||||||
|
|||||||
+13
-1
@@ -1,12 +1,24 @@
|
|||||||
Installation
|
Installation
|
||||||
============
|
============
|
||||||
|
|
||||||
To run the stack:
|
To run the stack in production:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker compose -f docker\docker-compose-prod.yaml -p event-demo up -d
|
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:
|
Api url:
|
||||||
- [Backend API](http://api.traefik.me/)
|
- [Backend API](http://api.traefik.me/)
|
||||||
- [Frontend web site](http://app.traefik.me/) (WIP)
|
- [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
|
# 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
|
RUN mkdir -p /home/gradle/cache_home
|
||||||
ENV GRADLE_USER_HOME=/home/gradle/cache_home
|
ENV GRADLE_USER_HOME=/home/gradle/cache_home
|
||||||
COPY build.gradle.* gradle.properties /home/gradle/app/
|
COPY build.gradle.* gradle.properties /home/gradle/app/
|
||||||
@@ -7,7 +7,7 @@ WORKDIR /home/gradle/app
|
|||||||
RUN gradle build -i -x check
|
RUN gradle build -i -x check
|
||||||
|
|
||||||
# Stage 2: Build Application
|
# 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 --from=cache /home/gradle/cache_home /home/gradle/.gradle
|
||||||
COPY --chown=gradle:gradle . /home/gradle/src
|
COPY --chown=gradle:gradle . /home/gradle/src
|
||||||
WORKDIR /home/gradle/src
|
WORKDIR /home/gradle/src
|
||||||
@@ -16,8 +16,8 @@ WORKDIR /home/gradle/src
|
|||||||
RUN gradle buildFatJar --no-daemon
|
RUN gradle buildFatJar --no-daemon
|
||||||
|
|
||||||
# Stage 3: Create the Runtime Image
|
# Stage 3: Create the Runtime Image
|
||||||
FROM amazoncorretto:21 AS runtime
|
FROM eclipse-temurin:21-jre-alpine AS runtime
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
RUN mkdir /app
|
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"]
|
ENTRYPOINT ["java","-jar","/app/event-demo-all.jar"]
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Image officielle Gradle avec JDK 21 déjà installé
|
# Image officielle Gradle avec JDK 21 déjà installé
|
||||||
FROM gradle:9.6.1-jdk21
|
FROM gradle:9.6.1-jdk21
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copie du wrapper et des fichiers de config en premier pour profiter du cache Docker
|
# Copie du wrapper et des fichiers de config en premier pour profiter du cache Docker
|
||||||
COPY build.gradle.kts settings.gradle.kts ./
|
COPY build.gradle.kts settings.gradle.kts ./
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
name: event-demo-ci
|
|
||||||
include:
|
|
||||||
- path:
|
|
||||||
- parts/docker-compose-databases.yaml
|
|
||||||
- parts/docker-compose-databases-expose.yaml
|
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
name: event-demo-test
|
name: event-demo-dev
|
||||||
include:
|
include:
|
||||||
- path:
|
- path:
|
||||||
- parts/docker-compose-databases.yaml
|
- parts/docker-compose-databases.yaml
|
||||||
- parts/docker-compose-databases-expose.yaml
|
- parts/docker-compose-databases-expose.yaml
|
||||||
- parts/docker-compose-tools.yaml
|
- parts/docker-compose-tools.yaml
|
||||||
- parts/docker-compose-tools-local.yaml
|
- parts/docker-compose-tools-local.yaml
|
||||||
- parts/docker-compose-traefik.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-app.yaml
|
||||||
- parts/docker-compose-tools.yaml
|
- parts/docker-compose-tools.yaml
|
||||||
- parts/docker-compose-traefik.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
|
||||||
@@ -3,4 +3,9 @@ include:
|
|||||||
- path:
|
- path:
|
||||||
- parts/docker-compose-databases.yaml
|
- parts/docker-compose-databases.yaml
|
||||||
- parts/docker-compose-test.yaml
|
- parts/docker-compose-test.yaml
|
||||||
- parts/docker-compose-traefik.yaml
|
- parts/docker-compose-traefik.yaml
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgresql:
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: "changeit"
|
||||||
@@ -12,6 +12,8 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- ../.env.docker
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.api.rule=Host(`api.traefik.me`)"
|
- "traefik.http.routers.api.rule=Host(`api.traefik.me`)"
|
||||||
- "traefik.http.services.api.loadbalancer.server.port=8080"
|
- "traefik.http.services.api.loadbalancer.server.port=8080"
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ services:
|
|||||||
image: postgres:18.4
|
image: postgres:18.4
|
||||||
command: postgres -c 'max_connections=500'
|
command: postgres -c 'max_connections=500'
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD_FILE: /run/secrets/postgresql_password
|
|
||||||
POSTGRES_USER: event-demo
|
POSTGRES_USER: event-demo
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "sh -c 'pg_isready -U event-demo'"]
|
test: ["CMD-SHELL", "sh -c 'pg_isready -U event-demo'"]
|
||||||
@@ -31,7 +30,6 @@ services:
|
|||||||
retries: 10
|
retries: 10
|
||||||
volumes:
|
volumes:
|
||||||
- postgresql_data:/var/lib/postgresql
|
- postgresql_data:/var/lib/postgresql
|
||||||
- ../postgresql.secret:/run/secrets/postgresql_password:ro
|
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq:4-management-alpine
|
image: rabbitmq:4-management-alpine
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- ../.env.docker
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
gradle-cache:
|
gradle-cache:
|
||||||
@@ -2,10 +2,7 @@ services:
|
|||||||
pgadmin:
|
pgadmin:
|
||||||
image: dpage/pgadmin4
|
image: dpage/pgadmin4
|
||||||
environment:
|
environment:
|
||||||
PGADMIN_DEFAULT_EMAIL: $PGADMIN_DEFAULT_EMAIL
|
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@event-demo.dev}
|
||||||
PGADMIN_DEFAULT_PASSWORD_FILE: /run/secrets/pgadmin_password
|
|
||||||
secrets:
|
|
||||||
- pgadmin_password
|
|
||||||
volumes:
|
volumes:
|
||||||
- pgadmin_data:/var/lib/pgadmin
|
- pgadmin_data:/var/lib/pgadmin
|
||||||
configs:
|
configs:
|
||||||
@@ -45,9 +42,5 @@ configs:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets:
|
|
||||||
pgadmin_password:
|
|
||||||
file: ../pgadmin.secret
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgadmin_data:
|
pgadmin_data:
|
||||||
@@ -13,12 +13,12 @@ jwt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redis {
|
redis {
|
||||||
url = "redis://redis:6379"
|
url = "redis://localhost:6379"
|
||||||
url = ${?REDIS_URL}
|
url = ${?REDIS_URL}
|
||||||
}
|
}
|
||||||
|
|
||||||
postgresql {
|
postgresql {
|
||||||
url = "jdbc:postgresql://postgresql/event-demo"
|
url = "jdbc:postgresql://localhost:5432/event-demo"
|
||||||
url = ${?POSTGRESQL_URL}
|
url = ${?POSTGRESQL_URL}
|
||||||
|
|
||||||
username = "event-demo"
|
username = "event-demo"
|
||||||
@@ -29,7 +29,7 @@ postgresql {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rabbitmq {
|
rabbitmq {
|
||||||
url = "rabbitmq"
|
url = "localhost"
|
||||||
url = ${?RABBITMQ_URL}
|
url = ${?RABBITMQ_URL}
|
||||||
|
|
||||||
port = "5672"
|
port = "5672"
|
||||||
|
|||||||
@@ -26,11 +26,13 @@ import eventDemo.testHelpers.createNewUser
|
|||||||
import eventDemo.testHelpers.testKoinApplicationWithConfig
|
import eventDemo.testHelpers.testKoinApplicationWithConfig
|
||||||
import io.kotest.assertions.nondeterministic.eventually
|
import io.kotest.assertions.nondeterministic.eventually
|
||||||
import io.kotest.assertions.nondeterministic.until
|
import io.kotest.assertions.nondeterministic.until
|
||||||
|
import io.kotest.assertions.retry
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.kotest.matchers.collections.shouldContainExactly
|
import io.kotest.matchers.collections.shouldContainExactly
|
||||||
import io.kotest.matchers.equals.shouldBeEqual
|
import io.kotest.matchers.equals.shouldBeEqual
|
||||||
import io.kotest.matchers.equals.shouldEqual
|
import io.kotest.matchers.equals.shouldEqual
|
||||||
import io.kotest.matchers.nulls.shouldNotBeNull
|
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||||
|
import io.kotest.matchers.should
|
||||||
import io.kotest.matchers.types.shouldBeInstanceOf
|
import io.kotest.matchers.types.shouldBeInstanceOf
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -49,223 +51,225 @@ class GameSimulationTest :
|
|||||||
tags(Tag.Postgresql)
|
tags(Tag.Postgresql)
|
||||||
|
|
||||||
test("Simulation of a game") {
|
test("Simulation of a game") {
|
||||||
withTimeout(10.seconds) {
|
should {
|
||||||
disableRandomForTest()
|
retry(maxRetry = 3, timeout = 20.seconds) {
|
||||||
val gameId = GameId()
|
disableRandomForTest()
|
||||||
val user1 = createNewUser("user1")
|
val gameId = GameId()
|
||||||
val user2 = createNewUser("user2")
|
val user1 = createNewUser("user1")
|
||||||
|
val user2 = createNewUser("user2")
|
||||||
|
|
||||||
val channelCommand1 = Channel<GameCommand>(Channel.BUFFERED)
|
val channelCommand1 = Channel<GameCommand>(Channel.BUFFERED)
|
||||||
val channelCommand2 = Channel<GameCommand>(Channel.BUFFERED)
|
val channelCommand2 = Channel<GameCommand>(Channel.BUFFERED)
|
||||||
val channelNotification1 = Channel<Notification>(Channel.BUFFERED)
|
val channelNotification1 = Channel<Notification>(Channel.BUFFERED)
|
||||||
val channelNotification2 = Channel<Notification>(Channel.BUFFERED)
|
val channelNotification2 = Channel<Notification>(Channel.BUFFERED)
|
||||||
|
|
||||||
var playedCard1: Card? = null
|
var playedCard1: Card? = null
|
||||||
var playedCard2: Card? = null
|
var playedCard2: Card? = null
|
||||||
|
|
||||||
var player1HasJoin = false
|
var player1HasJoin = false
|
||||||
|
|
||||||
testKoinApplicationWithConfig {
|
testKoinApplicationWithConfig {
|
||||||
val gameRepository = get<GameRepository>()
|
val gameRepository = get<GameRepository>()
|
||||||
val userRepository = get<UserRepository>()
|
val userRepository = get<UserRepository>()
|
||||||
userRepository.run {
|
userRepository.run {
|
||||||
save(user1)
|
save(user1)
|
||||||
save(user2)
|
save(user2)
|
||||||
}
|
}
|
||||||
|
|
||||||
gameRepository.create(gameId)
|
gameRepository.create(gameId)
|
||||||
|
|
||||||
// Run command/notification subscriber
|
// Run command/notification subscriber
|
||||||
// In the normal process, these subscriber is invoque on players connect to the websocket
|
// In the normal process, these subscriber is invoque on players connect to the websocket
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
get<GameChannelsSubscriber>().subscribePlayerToGameChannels(
|
get<GameChannelsSubscriber>().subscribePlayerToGameChannels(
|
||||||
gameId,
|
gameId,
|
||||||
user1.id,
|
user1.id,
|
||||||
channelCommand1,
|
channelCommand1,
|
||||||
channelNotification1,
|
channelNotification1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
get<GameChannelsSubscriber>().subscribePlayerToGameChannels(
|
get<GameChannelsSubscriber>().subscribePlayerToGameChannels(
|
||||||
gameId,
|
gameId,
|
||||||
user2.id,
|
user2.id,
|
||||||
channelCommand2,
|
channelCommand2,
|
||||||
channelNotification2,
|
channelNotification2,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume etch notification of players, and put theses in a list.
|
// Consume etch notification of players, and put theses in a list.
|
||||||
// Is used later to control when other players can execute the next action
|
// Is used later to control when other players can execute the next action
|
||||||
val player1Notifications = mutableListOf<Notification>()
|
val player1Notifications = mutableListOf<Notification>()
|
||||||
val player2Notifications = mutableListOf<Notification>()
|
val player2Notifications = mutableListOf<Notification>()
|
||||||
run {
|
run {
|
||||||
GlobalScope.launch {
|
GlobalScope.launch {
|
||||||
for (notification in channelNotification1) {
|
for (notification in channelNotification1) {
|
||||||
player1Notifications.add(notification)
|
player1Notifications.add(notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalScope.launch {
|
||||||
|
for (notification in channelNotification2) {
|
||||||
|
player2Notifications.add(notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GlobalScope.launch {
|
// Player 1 actions
|
||||||
for (notification in channelNotification2) {
|
val player1Job =
|
||||||
player2Notifications.add(notification)
|
launch {
|
||||||
}
|
createGameWithCommandsInChannels(channelCommand1, gameId, user1) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player 1 actions
|
joinTheGame()
|
||||||
val player1Job =
|
player1Notifications.waitNotification<WelcomeToTheGameNotification> {
|
||||||
launch {
|
players.map { it.userId }.contains(user1.id)
|
||||||
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!!)
|
player1HasJoin = true
|
||||||
|
|
||||||
player1Notifications.waitNotification<ItsTheTurnOfNotification> {
|
player1Notifications.waitNotification<PlayerAsJoinTheGameNotification> {
|
||||||
player == getPlayer(user2)
|
player.userId == user2.id
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
readyToPlay()
|
||||||
|
player1Notifications.waitNotification<PlayerWasReadyNotification> {
|
||||||
|
playerId == getPlayer(user2).id
|
||||||
|
}
|
||||||
|
|
||||||
player2Notifications.waitNotification<ItsTheTurnOfNotification> {
|
playedCard1 =
|
||||||
player.userId == user1.id
|
player1Notifications
|
||||||
}
|
.waitNotification<TheGameWasStartedNotification> { hand.size == 7 }
|
||||||
player2Notifications.waitNotification<PlayerAsPlayACardNotification> {
|
.hand
|
||||||
playerId == currentPlayer.id && card == playedCard2
|
.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
|
// Player 2 actions
|
||||||
joinAll(player1Job, player2Job)
|
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
|
joinTheGame()
|
||||||
val game = gameRepository.get(gameId)
|
|
||||||
assertInstanceOf<GameStarted>(game)
|
|
||||||
|
|
||||||
// Check if the state is correct
|
player2Notifications.waitNotification<WelcomeToTheGameNotification> {
|
||||||
game.aggregateId shouldBeEqual gameId
|
players.map { it.userId }.contains(user1.id) &&
|
||||||
game.players.map { it.userId } shouldContainExactly setOf(user1.id, user2.id)
|
players.map { it.userId }.contains(user2.id)
|
||||||
assertNotNull(game.players.find { it.userId == user1.id })
|
}
|
||||||
.hand.size shouldBeEqual 5
|
player2Notifications.waitNotification<PlayerWasReadyNotification> { playerId == getPlayer(user1).id }
|
||||||
assertNotNull(game.players.find { it.userId == user2.id })
|
|
||||||
.hand.size shouldBeEqual 6
|
readyToPlay()
|
||||||
game.direction shouldBeEqual Game.Direction.CLOCKWISE
|
|
||||||
assertNotNull(game.lastPlayer?.userId) shouldBeEqual user1.id
|
playedCard2 =
|
||||||
assertNotNull(game.discardPile.topCard) shouldBeEqual assertNotNull(playedCard1)
|
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)
|
context(user: User)
|
||||||
private suspend inline fun <reified T : Notification> MutableList<Notification>.waitNotification(crossinline block: T.() -> Boolean): T {
|
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}")
|
println("NOTIFICATION WAITING: ${T::class.simpleName} for user: ${user.username}")
|
||||||
return eventually(3.seconds) {
|
return eventually(5.seconds) {
|
||||||
filterIsInstance<T>()
|
filterIsInstance<T>()
|
||||||
.first { block(it) }
|
.first { block(it) }
|
||||||
.also { remove(it) }
|
.also { remove(it) }
|
||||||
|
|||||||
Reference in New Issue
Block a user