diff --git a/build.gradle.kts b/build.gradle.kts index 4bd559ef78b96f41a6eb22d4a3d101a3b2cb6d43..f44520855349d1923749cf1c2977690d8813f734 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,7 @@ val localstack_version: String by project plugins { kotlin("jvm") version "2.1.10" id("io.ktor.plugin") version "2.3.13" + kotlin("plugin.serialization") version "1.9.0" } group = "betclic.test" @@ -43,7 +44,7 @@ dependencies { // Content negociation implementation("io.ktor:ktor-server-content-negotiation") - implementation("io.ktor:ktor-serialization-jackson") + implementation("io.ktor:ktor-serialization-kotlinx-json") implementation("ch.qos.logback:logback-classic:$logback_version") // Database diff --git a/src/main/kotlin/configuration/ApiConfiguration.kt b/src/main/kotlin/configuration/ApiConfiguration.kt index 27f2a9c87fedd2c1e639985e042dd0e4d602042d..7e4ac308798de15c77c22c29c4f3d5532a62d0dc 100644 --- a/src/main/kotlin/configuration/ApiConfiguration.kt +++ b/src/main/kotlin/configuration/ApiConfiguration.kt @@ -1,14 +1,11 @@ package betclic.test.configuration -import com.fasterxml.jackson.databind.SerializationFeature -import io.ktor.serialization.jackson.* +import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* import io.ktor.server.plugins.contentnegotiation.* fun Application.configureSerialization() { install(ContentNegotiation) { - jackson { - enable(SerializationFeature.INDENT_OUTPUT) - } + json() } } diff --git a/src/main/kotlin/player/PlayerRepository.kt b/src/main/kotlin/player/PlayerRepository.kt index 4360500d0803da9398e3823ea6281f8a02fafad0..8aae63430a6dc0b81568305feeb5e6765c983a98 100644 --- a/src/main/kotlin/player/PlayerRepository.kt +++ b/src/main/kotlin/player/PlayerRepository.kt @@ -25,7 +25,6 @@ class PlayerRepository(dynamoDbEnhancedClient: DynamoDbEnhancedAsyncClient) { } suspend fun updatePlayer(player: Player): Player { - val updatedPlayer = table.updateItem(player.toPlayerEntity()).await() return updatedPlayer.toPlayer() } diff --git a/src/main/kotlin/player/dtos/PlayerCreationDTO.kt b/src/main/kotlin/player/dtos/PlayerCreationDTO.kt index 46568cf02ccce1602fe30a7fee29b0ba82750455..eb150a1199ec5131036b02b32fba63bf27de4b68 100644 --- a/src/main/kotlin/player/dtos/PlayerCreationDTO.kt +++ b/src/main/kotlin/player/dtos/PlayerCreationDTO.kt @@ -1,7 +1,9 @@ package betclic.test.player.dtos import betclic.test.player.Player +import kotlinx.serialization.Serializable +@Serializable data class PlayerCreationDTO( val pseudo: String, ) diff --git a/src/main/kotlin/player/dtos/PlayerInfoDTO.kt b/src/main/kotlin/player/dtos/PlayerInfoDTO.kt index abeff58a21c33077025e4bd4eddef6029caef79f..16b8490dd36ab2cf6836b3be6e5d2f60a767d26b 100644 --- a/src/main/kotlin/player/dtos/PlayerInfoDTO.kt +++ b/src/main/kotlin/player/dtos/PlayerInfoDTO.kt @@ -1,7 +1,9 @@ package betclic.test.player.dtos import betclic.test.player.Player +import kotlinx.serialization.Serializable +@Serializable data class PlayerInfoDTO( val pseudo: String, val pointsNumber: Int, diff --git a/src/main/kotlin/player/dtos/PlayerUpdateDTO.kt b/src/main/kotlin/player/dtos/PlayerUpdateDTO.kt index b209e4cf36d66173d46542a82d773a09916d0572..ca5f03e0c8f200886db3039b684fe1b0968c6e80 100644 --- a/src/main/kotlin/player/dtos/PlayerUpdateDTO.kt +++ b/src/main/kotlin/player/dtos/PlayerUpdateDTO.kt @@ -1,7 +1,9 @@ package betclic.test.player.dtos import betclic.test.player.Player +import kotlinx.serialization.Serializable +@Serializable data class PlayerUpdateDTO( val pseudo: String, val pointsNumber: Int, diff --git a/src/test/kotlin/player/PlayerIntegrationTest.kt b/src/test/kotlin/player/PlayerIntegrationTest.kt index ea7d9bb744ab68501fcf43beefcf40838b417159..4585d90dcd6c21d888d65a63beae19da32e374b0 100644 --- a/src/test/kotlin/player/PlayerIntegrationTest.kt +++ b/src/test/kotlin/player/PlayerIntegrationTest.kt @@ -2,8 +2,11 @@ package player import BaseIntegrationTest import betclic.test.player.PlayerRepository +import betclic.test.player.dtos.PlayerCreationDTO import io.ktor.client.request.* import io.ktor.http.* +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import org.assertj.core.api.Assertions.assertThat import org.junit.Test import org.koin.test.inject @@ -17,8 +20,10 @@ class PlayerIntegrationTest : BaseIntegrationTest() { @Test fun `When calling player creation, a player should be saved in DB`() = iTest { val response = client.post("/players") { - header(HttpHeaders.ContentType, ContentType.Text.Plain) - setBody(PLAYER_NAME) + val player = PlayerCreationDTO(pseudo = PLAYER_NAME) + val json = Json.encodeToString(player) + header(HttpHeaders.ContentType, ContentType.Application.Json) + setBody(json) } assertEquals(HttpStatusCode.Created, response.status) diff --git a/src/test/kotlin/player/PlayerServiceTest.kt b/src/test/kotlin/player/PlayerServiceTest.kt index 9671150b8ac3ad4f52c0ccf7c5454fd355d3981e..c0a592fd5da6a08925b3da7a6fb1fea4acb79ac1 100644 --- a/src/test/kotlin/player/PlayerServiceTest.kt +++ b/src/test/kotlin/player/PlayerServiceTest.kt @@ -1,11 +1,20 @@ -package betclic.test.player +package player +import betclic.test.player.Player +import betclic.test.player.PlayerRepository +import betclic.test.player.PlayerServiceImpl +import betclic.test.player.dtos.PlayerCreationDTO +import betclic.test.player.dtos.PlayerUpdateDTO +import betclic.test.player.exceptions.AlreadyExistingPlayerException +import io.ktor.server.plugins.* import io.mockk.coEvery import io.mockk.coVerify import io.mockk.just import io.mockk.mockk import io.mockk.runs import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.assertThrows import org.junit.Test class PlayerServiceTest { @@ -15,13 +24,88 @@ class PlayerServiceTest { private val playerService: PlayerServiceImpl = PlayerServiceImpl(playerRepository) private val john = "John" + private val doe = "Doe" private val player1 = Player(pseudo = john) + private val playerToUpdate = Player(pseudo = john, pointsNumber = 10) + private val player2 = Player(pseudo = doe, pointsNumber = 20) @Test - fun `should create a new player in database`() { - coEvery { playerRepository.createNewPlayer(player1) } just runs - runBlocking { playerService.createNewPlayer(john) } + fun `when creating player, should call the repository once`() { + coEvery { playerRepository.createNewPlayer(player1) } returns player1 + coEvery { playerRepository.findPlayerByPseudo(john) } returns null + runBlocking { playerService.createNewPlayer(PlayerCreationDTO(pseudo = john)) } coVerify(exactly = 1) { playerRepository.createNewPlayer(player1) } } + @Test + fun `when creating player that exists, should throw on existing player`() { + coEvery { playerRepository.findPlayerByPseudo(john) } returns player1 + assertThrows(AlreadyExistingPlayerException::class.java) { + runBlocking { playerService.createNewPlayer(PlayerCreationDTO(pseudo = john)) } + } + } + + @Test + fun `when updating player, should call the repository once`() { + coEvery { playerRepository.findPlayerByPseudo(pseudo = john) } returns player1 + coEvery { playerRepository.updatePlayer(playerToUpdate) } returns playerToUpdate + runBlocking { playerService.updatePlayer(PlayerUpdateDTO(pseudo = john, pointsNumber = 10)) } + coVerify(exactly = 1) { playerRepository.updatePlayer(playerToUpdate) } + } + + @Test + fun `when updating player that doesn't exist, should throw on not found player`() { + coEvery { playerRepository.findPlayerByPseudo(pseudo = john) } returns null + assertThrows(NotFoundException::class.java) { + runBlocking { playerService.updatePlayer(PlayerUpdateDTO(pseudo = john, pointsNumber = 10)) } + } + } + + @Test + fun `when finding user by pseudo, should call the repository once`() { + coEvery { playerRepository.findPlayerByPseudo(pseudo = john) } returns player1 + runBlocking { playerService.findPlayerByPseudo(pseudo = john) } + coVerify(exactly = 1) { playerRepository.findPlayerByPseudo(john) } + } + + @Test + fun `when finding by pseudo, should throw on not found player`() { + coEvery { playerRepository.findPlayerByPseudo(pseudo = john) } returns null + assertThrows(NotFoundException::class.java) { + runBlocking { playerService.findPlayerByPseudo(pseudo = john) } + } + } + + @Test + fun `when getting player info, should return player with his rank`() { + coEvery { playerRepository.findPlayerByPseudo(pseudo = john) } returns player1 + coEvery { playerRepository.getRank(player1) } returns 2 + val result = runBlocking { playerService.getPlayerInfoByPseudo(pseudo = john) } + assertThat(result.pseudo).isEqualTo(john) + assertThat(result.ranking).isEqualTo(2) + } + + @Test + fun `when getting player info on non-existing player, should throw on not found player`() { + coEvery { playerRepository.findPlayerByPseudo(pseudo = john) } returns null + coEvery { playerRepository.getRank(player1) } returns 2 + assertThrows(NotFoundException::class.java) { + runBlocking { playerService.getPlayerInfoByPseudo(pseudo = john) } + } + } + + @Test + fun `when getting all players ranking, should return players sorted by rank`() { + coEvery { playerRepository.findAll() } returns listOf(player1, player2) + val result = runBlocking { playerService.getPlayersRanked() } + assertThat(result.first().ranking).isEqualTo(1) + assertThat(result.last().ranking).isEqualTo(2) + } + + @Test + fun `when deleting all players, should call the repository once`() { + coEvery { playerRepository.deleteAllPlayers() } just runs + runBlocking { playerService.deleteAllPlayers() } + coVerify(exactly = 1) { playerRepository.deleteAllPlayers() } + } } \ No newline at end of file diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml index 2e745a4a2053187e0d989767ef16b3b8c977433a..00ca02f1cdf950d163f304c3759e8ed4b1591008 100644 --- a/src/test/resources/application-test.yaml +++ b/src/test/resources/application-test.yaml @@ -1,7 +1,7 @@ ktor: application: modules: - - betclic.test.TestApplicationKt.module + - TestApplicationKt.module deployment: port: 8081 database: