core: add NekoService, remove serialization annotations from dto

This commit is contained in:
Savosin Denis
2025-06-03 14:38:55 +07:00
parent b817da4a72
commit 76e4af62ae
11 changed files with 228 additions and 40 deletions

View File

@@ -1,22 +1,14 @@
package com.github.dannecron.demo.core.dto 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.time.OffsetDateTime
import java.util.UUID import java.util.UUID
@Serializable
data class City( data class City(
val id: Long, val id: Long,
@Serializable(with = UuidSerialization::class)
val guid: UUID, val guid: UUID,
val name: String, val name: String,
@Serializable(with = OffsetDateTimeSerialization::class)
val createdAt: OffsetDateTime, val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
val updatedAt: OffsetDateTime?, val updatedAt: OffsetDateTime?,
@Serializable(with = OffsetDateTimeSerialization::class)
val deletedAt: OffsetDateTime?, val deletedAt: OffsetDateTime?,
) { ) {
fun isDeleted(): Boolean = deletedAt != null fun isDeleted(): Boolean = deletedAt != null

View File

@@ -1,20 +1,13 @@
package com.github.dannecron.demo.core.dto 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.time.OffsetDateTime
import java.util.UUID import java.util.UUID
@Serializable
data class Customer( data class Customer(
val id: Long, val id: Long,
@Serializable(with = UuidSerialization::class)
val guid: UUID, val guid: UUID,
val name: String, val name: String,
val cityId: Long?, val cityId: Long?,
@Serializable(with = OffsetDateTimeSerialization::class)
val createdAt: OffsetDateTime, val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
val updatedAt: OffsetDateTime?, val updatedAt: OffsetDateTime?,
) )

View File

@@ -1,22 +1,14 @@
package com.github.dannecron.demo.core.dto 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.time.OffsetDateTime
import java.util.UUID import java.util.UUID
@Serializable
data class Order( data class Order(
val id: Long, val id: Long,
@Serializable(with = UuidSerialization::class)
val guid: UUID, val guid: UUID,
val customerId: Long, val customerId: Long,
@Serializable(with = OffsetDateTimeSerialization::class)
val deliveredAt: OffsetDateTime?, val deliveredAt: OffsetDateTime?,
@Serializable(with = OffsetDateTimeSerialization::class)
val createdAt: OffsetDateTime, val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
val updatedAt: OffsetDateTime?, val updatedAt: OffsetDateTime?,
) { ) {
fun isDelivered(): Boolean = deliveredAt != null fun isDelivered(): Boolean = deliveredAt != null

View File

@@ -1,19 +1,12 @@
package com.github.dannecron.demo.core.dto 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.time.OffsetDateTime
import java.util.UUID import java.util.UUID
@Serializable
data class OrderProduct( data class OrderProduct(
@Serializable(with = UuidSerialization::class)
val guid: UUID, val guid: UUID,
val orderId: Long, val orderId: Long,
val productId: Long, val productId: Long,
@Serializable(with = OffsetDateTimeSerialization::class)
val createdAt: OffsetDateTime, val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
val updatedAt: OffsetDateTime?, val updatedAt: OffsetDateTime?,
) )

View File

@@ -1,26 +1,18 @@
package com.github.dannecron.demo.core.dto 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.time.OffsetDateTime
import java.util.UUID import java.util.UUID
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Serializable
data class Product( data class Product(
val id: Long, val id: Long,
@Serializable(with = UuidSerialization::class)
val guid: UUID, val guid: UUID,
val name: String, val name: String,
val description: String?, val description: String?,
val price: Long, val price: Long,
@Serializable(with = OffsetDateTimeSerialization::class)
val createdAt: OffsetDateTime, val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
val updatedAt: OffsetDateTime?, val updatedAt: OffsetDateTime?,
@Serializable(with = OffsetDateTimeSerialization::class)
val deletedAt: OffsetDateTime?, val deletedAt: OffsetDateTime?,
) { ) {
fun getPriceDouble(): Double = (price.toDouble() / 100).roundTo(2) fun getPriceDouble(): Double = (price.toDouble() / 100).roundTo(2)

View File

@@ -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?,
)

View File

@@ -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.City
import com.github.dannecron.demo.core.dto.Customer import com.github.dannecron.demo.core.dto.Customer
import kotlinx.serialization.Serializable
@Serializable
data class CustomerExtended( data class CustomerExtended(
val customer: Customer, val customer: Customer,
val city: City?, val city: City?,

View File

@@ -0,0 +1,6 @@
package com.github.dannecron.demo.core.exceptions.neko
class IntegrationException(
override val message: String,
override val cause: Throwable? = null,
) : RuntimeException()

View File

@@ -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<String>
@Throws(IntegrationException::class)
fun getImages(category: String, amount: Int): List<ImageDto>
}

View File

@@ -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<String> =
try {
nekoClient.getCategories()
} catch (ex: RequestException) {
throw IntegrationException("Neko request error", ex)
}
override fun getImages(category: String, amount: Int): List<ImageDto> =
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,
)
}

View File

@@ -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<IntegrationException> {
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<IntegrationException> {
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)
}
}