From 76e4af62aefb317094b67929fdd4c01cd9e7d3c5 Mon Sep 17 00:00:00 2001 From: Savosin Denis Date: Tue, 3 Jun 2025 14:38:55 +0700 Subject: [PATCH] core: add NekoService, remove serialization annotations from dto --- .../github/dannecron/demo/core/dto/City.kt | 8 - .../dannecron/demo/core/dto/Customer.kt | 7 - .../github/dannecron/demo/core/dto/Order.kt | 8 - .../dannecron/demo/core/dto/OrderProduct.kt | 7 - .../github/dannecron/demo/core/dto/Product.kt | 8 - .../dannecron/demo/core/dto/neko/ImageDto.kt | 9 + .../demo/core/dto/view/CustomerExtended.kt | 2 - .../exceptions/neko/IntegrationException.kt | 6 + .../demo/core/services/neko/NekoService.kt | 13 ++ .../core/services/neko/NekoServiceImpl.kt | 35 ++++ .../core/services/neko/NekoServiceImplTest.kt | 165 ++++++++++++++++++ 11 files changed, 228 insertions(+), 40 deletions(-) create mode 100644 core/src/main/kotlin/com/github/dannecron/demo/core/dto/neko/ImageDto.kt create mode 100644 core/src/main/kotlin/com/github/dannecron/demo/core/exceptions/neko/IntegrationException.kt create mode 100644 core/src/main/kotlin/com/github/dannecron/demo/core/services/neko/NekoService.kt create mode 100644 core/src/main/kotlin/com/github/dannecron/demo/core/services/neko/NekoServiceImpl.kt create mode 100644 core/src/test/kotlin/com/github/dannecron/demo/core/services/neko/NekoServiceImplTest.kt diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/City.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/City.kt index 6f5c37c..dcbb55e 100644 --- a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/City.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/City.kt @@ -1,22 +1,14 @@ package com.github.dannecron.demo.core.dto -import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization -import com.github.dannecron.demo.db.serialialization.UuidSerialization -import kotlinx.serialization.Serializable import java.time.OffsetDateTime import java.util.UUID -@Serializable data class City( val id: Long, - @Serializable(with = UuidSerialization::class) val guid: UUID, val name: String, - @Serializable(with = OffsetDateTimeSerialization::class) val createdAt: OffsetDateTime, - @Serializable(with = OffsetDateTimeSerialization::class) val updatedAt: OffsetDateTime?, - @Serializable(with = OffsetDateTimeSerialization::class) val deletedAt: OffsetDateTime?, ) { fun isDeleted(): Boolean = deletedAt != null diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Customer.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Customer.kt index 9404ac6..0578b69 100644 --- a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Customer.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Customer.kt @@ -1,20 +1,13 @@ package com.github.dannecron.demo.core.dto -import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization -import com.github.dannecron.demo.db.serialialization.UuidSerialization -import kotlinx.serialization.Serializable import java.time.OffsetDateTime import java.util.UUID -@Serializable data class Customer( val id: Long, - @Serializable(with = UuidSerialization::class) val guid: UUID, val name: String, val cityId: Long?, - @Serializable(with = OffsetDateTimeSerialization::class) val createdAt: OffsetDateTime, - @Serializable(with = OffsetDateTimeSerialization::class) val updatedAt: OffsetDateTime?, ) diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Order.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Order.kt index 1793971..b1fa254 100644 --- a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Order.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Order.kt @@ -1,22 +1,14 @@ package com.github.dannecron.demo.core.dto -import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization -import com.github.dannecron.demo.db.serialialization.UuidSerialization -import kotlinx.serialization.Serializable import java.time.OffsetDateTime import java.util.UUID -@Serializable data class Order( val id: Long, - @Serializable(with = UuidSerialization::class) val guid: UUID, val customerId: Long, - @Serializable(with = OffsetDateTimeSerialization::class) val deliveredAt: OffsetDateTime?, - @Serializable(with = OffsetDateTimeSerialization::class) val createdAt: OffsetDateTime, - @Serializable(with = OffsetDateTimeSerialization::class) val updatedAt: OffsetDateTime?, ) { fun isDelivered(): Boolean = deliveredAt != null diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/OrderProduct.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/OrderProduct.kt index 9995ad0..ec0c978 100644 --- a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/OrderProduct.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/OrderProduct.kt @@ -1,19 +1,12 @@ package com.github.dannecron.demo.core.dto -import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization -import com.github.dannecron.demo.db.serialialization.UuidSerialization -import kotlinx.serialization.Serializable import java.time.OffsetDateTime import java.util.UUID -@Serializable data class OrderProduct( - @Serializable(with = UuidSerialization::class) val guid: UUID, val orderId: Long, val productId: Long, - @Serializable(with = OffsetDateTimeSerialization::class) val createdAt: OffsetDateTime, - @Serializable(with = OffsetDateTimeSerialization::class) val updatedAt: OffsetDateTime?, ) diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Product.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Product.kt index 140357a..afb9961 100644 --- a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Product.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Product.kt @@ -1,26 +1,18 @@ package com.github.dannecron.demo.core.dto -import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization -import com.github.dannecron.demo.db.serialialization.UuidSerialization -import kotlinx.serialization.Serializable import java.time.OffsetDateTime import java.util.UUID import kotlin.math.pow import kotlin.math.roundToInt -@Serializable data class Product( val id: Long, - @Serializable(with = UuidSerialization::class) val guid: UUID, val name: String, val description: String?, val price: Long, - @Serializable(with = OffsetDateTimeSerialization::class) val createdAt: OffsetDateTime, - @Serializable(with = OffsetDateTimeSerialization::class) val updatedAt: OffsetDateTime?, - @Serializable(with = OffsetDateTimeSerialization::class) val deletedAt: OffsetDateTime?, ) { fun getPriceDouble(): Double = (price.toDouble() / 100).roundTo(2) diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/neko/ImageDto.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/neko/ImageDto.kt new file mode 100644 index 0000000..528a92c --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/neko/ImageDto.kt @@ -0,0 +1,9 @@ +package com.github.dannecron.demo.core.dto.neko + +data class ImageDto( + val url: String, + val animeName: String?, + val artistHref: String?, + val artistName: String?, + val sourceUrl: String?, +) diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/view/CustomerExtended.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/view/CustomerExtended.kt index c661bb6..c6ad3d9 100644 --- a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/view/CustomerExtended.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/view/CustomerExtended.kt @@ -2,9 +2,7 @@ package com.github.dannecron.demo.core.dto.view import com.github.dannecron.demo.core.dto.City import com.github.dannecron.demo.core.dto.Customer -import kotlinx.serialization.Serializable -@Serializable data class CustomerExtended( val customer: Customer, val city: City?, diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/exceptions/neko/IntegrationException.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/exceptions/neko/IntegrationException.kt new file mode 100644 index 0000000..aab0b00 --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/exceptions/neko/IntegrationException.kt @@ -0,0 +1,6 @@ +package com.github.dannecron.demo.core.exceptions.neko + +class IntegrationException( + override val message: String, + override val cause: Throwable? = null, +) : RuntimeException() diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/services/neko/NekoService.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/services/neko/NekoService.kt new file mode 100644 index 0000000..e1387db --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/services/neko/NekoService.kt @@ -0,0 +1,13 @@ +package com.github.dannecron.demo.core.services.neko + +import com.github.dannecron.demo.core.dto.neko.ImageDto +import com.github.dannecron.demo.core.exceptions.neko.IntegrationException + +interface NekoService { + + @Throws(IntegrationException::class) + fun getCategories(): Set + + @Throws(IntegrationException::class) + fun getImages(category: String, amount: Int): List +} diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/services/neko/NekoServiceImpl.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/services/neko/NekoServiceImpl.kt new file mode 100644 index 0000000..089ddb3 --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/services/neko/NekoServiceImpl.kt @@ -0,0 +1,35 @@ +package com.github.dannecron.demo.core.services.neko + +import com.github.dannecron.demo.core.dto.neko.ImageDto +import com.github.dannecron.demo.core.exceptions.neko.IntegrationException +import com.github.dannecron.demo.edgeintegration.client.neko.Client +import com.github.dannecron.demo.edgeintegration.client.neko.dto.Image +import com.github.dannecron.demo.edgeintegration.client.neko.exceptions.RequestException +import org.springframework.stereotype.Service + +@Service +class NekoServiceImpl( + private val nekoClient: Client +) : NekoService { + override fun getCategories(): Set = + try { + nekoClient.getCategories() + } catch (ex: RequestException) { + throw IntegrationException("Neko request error", ex) + } + + override fun getImages(category: String, amount: Int): List = + try { + nekoClient.getImages(category, amount).results.map { it.toCoreDto()} + } catch (ex: RequestException) { + throw IntegrationException("Neko request error", ex) + } + + private fun Image.toCoreDto() = ImageDto( + url = url, + animeName = animeName, + artistHref = artistHref, + artistName = artistName, + sourceUrl = sourceUrl, + ) +} diff --git a/core/src/test/kotlin/com/github/dannecron/demo/core/services/neko/NekoServiceImplTest.kt b/core/src/test/kotlin/com/github/dannecron/demo/core/services/neko/NekoServiceImplTest.kt new file mode 100644 index 0000000..75f9f1a --- /dev/null +++ b/core/src/test/kotlin/com/github/dannecron/demo/core/services/neko/NekoServiceImplTest.kt @@ -0,0 +1,165 @@ +package com.github.dannecron.demo.core.services.neko + +import com.github.dannecron.demo.core.exceptions.neko.IntegrationException +import com.github.dannecron.demo.edgeintegration.client.neko.Client +import com.github.dannecron.demo.edgeintegration.client.neko.dto.Image +import com.github.dannecron.demo.edgeintegration.client.neko.dto.ImagesResponse +import com.github.dannecron.demo.edgeintegration.client.neko.exceptions.RequestException +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * AI generated + */ +class NekoServiceImplTest { + + private val nekoClient: Client = mock() + private val nekoService: NekoService = NekoServiceImpl(nekoClient) + + @Test + fun `getCategories - success`() { + // Given + val expectedCategories = setOf("neko", "wink", "hug") + whenever(nekoClient.getCategories()).doReturn(expectedCategories) + + // When + val result = nekoService.getCategories() + + // Then + assertEquals(expectedCategories, result) + verify(nekoClient, times(1)).getCategories() + } + + @Test + fun `getCategories - throws IntegrationException when client throws RequestException`() { + // Given + val requestException = RequestException("Client error") + whenever(nekoClient.getCategories()).doThrow(requestException) + + // When & Then + val exception = assertThrows { + nekoService.getCategories() + } + + assertEquals("Neko request error", exception.message) + assertEquals(requestException, exception.cause) + verify(nekoClient, times(1)).getCategories() + } + + @Test + fun `getImages - success - maps images correctly`() { + // Given + val category = "neko" + val amount = 2 + val image1 = Image( + url = "https://example.com/image1.png", + animeName = "Test Anime 1", + artistHref = "https://artist1.com", + artistName = "Artist 1", + sourceUrl = "https://source1.com" + ) + val image2 = Image( + url = "https://example.com/image2.gif", + animeName = null, + artistHref = null, + artistName = null, + sourceUrl = null + ) + val imagesResponse = ImagesResponse(results = listOf(image1, image2)) + whenever(nekoClient.getImages(category, amount)).doReturn(imagesResponse) + + // When + val result = nekoService.getImages(category, amount) + + // Then + assertEquals(2, result.size) + + val firstImage = result[0] + assertEquals("https://example.com/image1.png", firstImage.url) + assertEquals("Test Anime 1", firstImage.animeName) + assertEquals("https://artist1.com", firstImage.artistHref) + assertEquals("Artist 1", firstImage.artistName) + assertEquals("https://source1.com", firstImage.sourceUrl) + + val secondImage = result[1] + assertEquals("https://example.com/image2.gif", secondImage.url) + assertEquals(null, secondImage.animeName) + assertEquals(null, secondImage.artistHref) + assertEquals(null, secondImage.artistName) + assertEquals(null, secondImage.sourceUrl) + + verify(nekoClient, times(1)).getImages(category, amount) + } + + @Test + fun `getImages - success - empty results`() { + // Given + val category = "empty" + val amount = 1 + val imagesResponse = ImagesResponse(results = emptyList()) + whenever(nekoClient.getImages(category, amount)).doReturn(imagesResponse) + + // When + val result = nekoService.getImages(category, amount) + + // Then + assertTrue(result.isEmpty()) + verify(nekoClient, times(1)).getImages(category, amount) + } + + @Test + fun `getImages - throws IntegrationException when client throws RequestException`() { + // Given + val category = "neko" + val amount = 5 + val requestException = RequestException("Client error") + whenever(nekoClient.getImages(category, amount)).doThrow(requestException) + + // When & Then + val exception = assertThrows { + nekoService.getImages(category, amount) + } + + assertEquals("Neko request error", exception.message) + assertEquals(requestException, exception.cause) + verify(nekoClient, times(1)).getImages(category, amount) + } + + @Test + fun `getImages - success - single image with all fields null`() { + // Given + val category = "test" + val amount = 1 + val image = Image( + url = "https://example.com/minimal.jpg", + animeName = null, + artistHref = null, + artistName = null, + sourceUrl = null + ) + val imagesResponse = ImagesResponse(results = listOf(image)) + whenever(nekoClient.getImages(category, amount)).doReturn(imagesResponse) + + // When + val result = nekoService.getImages(category, amount) + + // Then + assertEquals(1, result.size) + val resultImage = result[0] + assertEquals("https://example.com/minimal.jpg", resultImage.url) + assertEquals(null, resultImage.animeName) + assertEquals(null, resultImage.artistHref) + assertEquals(null, resultImage.artistName) + assertEquals(null, resultImage.sourceUrl) + + verify(nekoClient, times(1)).getImages(category, amount) + } +}