From a8639f8d1532db04703d85df8a4a1bceb21462b4 Mon Sep 17 00:00:00 2001 From: Savosin Denis Date: Tue, 3 Jun 2025 11:34:28 +0700 Subject: [PATCH] add send method to product service --- core/build.gradle.kts | 3 +- .../core/exceptions/InvalidDataException.kt | 6 +++ .../exceptions/ProductNotFoundException.kt | 2 +- .../core/services/product/ProductService.kt | 4 ++ .../services/product/ProductServiceImpl.kt | 23 ++++++++++ .../product/ProductServiceImplTest.kt | 42 +++++++++++++++++++ .../http/controllers/ProductController.kt | 8 ++-- .../http/controllers/ProductControllerTest.kt | 11 ----- 8 files changed, 80 insertions(+), 19 deletions(-) create mode 100644 core/src/main/kotlin/com/github/dannecron/demo/core/exceptions/InvalidDataException.kt diff --git a/core/build.gradle.kts b/core/build.gradle.kts index cbb58df..669e7e2 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -3,11 +3,10 @@ version = "single-version" dependencies { implementation(project(":db")) + implementation(project(":edge-producing")) implementation(rootProject.libs.spring.boot.starter.actuator) implementation(rootProject.libs.spring.boot.starter.jdbc) - implementation(rootProject.libs.spring.boot.starter.validation) - implementation(rootProject.libs.json.schema.validator) testImplementation(rootProject.libs.spring.boot.starter.actuatorAutoconfigure) } diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/exceptions/InvalidDataException.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/exceptions/InvalidDataException.kt new file mode 100644 index 0000000..4d45d88 --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/exceptions/InvalidDataException.kt @@ -0,0 +1,6 @@ +package com.github.dannecron.demo.core.exceptions + +class InvalidDataException( + override val message: String, + override val cause: Throwable?, +) : RuntimeException(message, cause) diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/exceptions/ProductNotFoundException.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/exceptions/ProductNotFoundException.kt index e180b3c..27185f5 100644 --- a/core/src/main/kotlin/com/github/dannecron/demo/core/exceptions/ProductNotFoundException.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/exceptions/ProductNotFoundException.kt @@ -1,3 +1,3 @@ package com.github.dannecron.demo.core.exceptions -class ProductNotFoundException: ModelNotFoundException("product") +class ProductNotFoundException: ModelNotFoundException("json-schemas/kafka/product") diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/services/product/ProductService.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/services/product/ProductService.kt index 129b3c9..8bd8557 100644 --- a/core/src/main/kotlin/com/github/dannecron/demo/core/services/product/ProductService.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/services/product/ProductService.kt @@ -2,6 +2,7 @@ package com.github.dannecron.demo.core.services.product import com.github.dannecron.demo.core.dto.Product import com.github.dannecron.demo.core.exceptions.AlreadyDeletedException +import com.github.dannecron.demo.core.exceptions.InvalidDataException import com.github.dannecron.demo.core.exceptions.ProductNotFoundException import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable @@ -16,4 +17,7 @@ interface ProductService { @Throws(ProductNotFoundException::class, AlreadyDeletedException::class) fun delete(guid: UUID): Product + + @Throws(ProductNotFoundException::class, InvalidDataException::class) + fun send(guid: UUID, topic: String?) } diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/services/product/ProductServiceImpl.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/services/product/ProductServiceImpl.kt index 76034a5..31ecf9c 100644 --- a/core/src/main/kotlin/com/github/dannecron/demo/core/services/product/ProductServiceImpl.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/services/product/ProductServiceImpl.kt @@ -2,21 +2,26 @@ package com.github.dannecron.demo.core.services.product import com.github.dannecron.demo.core.dto.Product import com.github.dannecron.demo.core.exceptions.AlreadyDeletedException +import com.github.dannecron.demo.core.exceptions.InvalidDataException import com.github.dannecron.demo.core.exceptions.ProductNotFoundException import com.github.dannecron.demo.core.services.generation.CommonGenerator import com.github.dannecron.demo.core.utils.LoggerDelegate import com.github.dannecron.demo.db.entity.ProductEntity import com.github.dannecron.demo.db.repository.ProductRepository +import com.github.dannecron.demo.edgeproducing.dto.ProductDto +import com.github.dannecron.demo.edgeproducing.producer.ProductProducer import net.logstash.logback.marker.Markers import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service +import java.time.format.DateTimeFormatter import java.util.UUID @Service class ProductServiceImpl( private val productRepository: ProductRepository, private val commonGenerator: CommonGenerator, + private val productProducer: ProductProducer, ): ProductService { private val logger by LoggerDelegate() @@ -58,6 +63,13 @@ class ProductServiceImpl( .toCore() } + @Throws(ProductNotFoundException::class, InvalidDataException::class) + override fun send(guid: UUID, topic: String?) { + val product = findByGuid(guid) ?: throw ProductNotFoundException() + + productProducer.produceProductSync(product.toProducingDto()) + } + private fun ProductEntity.toCore() = Product( id = id!!, guid = guid, @@ -79,4 +91,15 @@ class ProductServiceImpl( updatedAt = updatedAt, deletedAt = deletedAt, ) + + private fun Product.toProducingDto() = ProductDto( + id = id, + guid = guid.toString(), + name = name, + description = description, + price = price, + createdAt = createdAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), + updatedAt = updatedAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), + deletedAt = deletedAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), + ) } diff --git a/core/src/test/kotlin/com/github/dannecron/demo/core/services/product/ProductServiceImplTest.kt b/core/src/test/kotlin/com/github/dannecron/demo/core/services/product/ProductServiceImplTest.kt index acc5731..0566464 100644 --- a/core/src/test/kotlin/com/github/dannecron/demo/core/services/product/ProductServiceImplTest.kt +++ b/core/src/test/kotlin/com/github/dannecron/demo/core/services/product/ProductServiceImplTest.kt @@ -6,6 +6,8 @@ import com.github.dannecron.demo.core.exceptions.ProductNotFoundException import com.github.dannecron.demo.core.services.generation.CommonGenerator import com.github.dannecron.demo.db.entity.ProductEntity import com.github.dannecron.demo.db.repository.ProductRepository +import com.github.dannecron.demo.edgeproducing.dto.ProductDto +import com.github.dannecron.demo.edgeproducing.producer.ProductProducer import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.mockito.kotlin.any @@ -14,8 +16,10 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter import java.util.UUID import kotlin.test.assertEquals @@ -24,6 +28,7 @@ class ProductServiceImplTest { private val mockCurrentTime = OffsetDateTime.now() private val productRepository: ProductRepository = mock() + private val productProducer: ProductProducer = mock() private val commonGenerator: CommonGenerator = mock { on { generateUUID() } doReturn mockGuid on { generateCurrentTime() } doReturn mockCurrentTime @@ -32,6 +37,7 @@ class ProductServiceImplTest { private val productService = ProductServiceImpl( productRepository = productRepository, commonGenerator = commonGenerator, + productProducer = productProducer, ) private val guid = UUID.randomUUID() @@ -55,6 +61,16 @@ class ProductServiceImplTest { updatedAt = mockCurrentTime.minusHours(2), deletedAt = null, ) + private val producingProductDto = ProductDto( + id = 123, + guid = guid.toString(), + name = "name", + description = "description", + price = 10050, + createdAt = mockCurrentTime.minusDays(1).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), + updatedAt = mockCurrentTime.minusHours(2).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), + deletedAt = null, + ) @Test fun create() { @@ -127,4 +143,30 @@ class ProductServiceImplTest { verify(productRepository, times(1)).findByGuid(guid) verify(productRepository, never()).save(any()) } + + @Test + fun `send - success`() { + val topic = "some-topic" + + whenever(productRepository.findByGuid(any())).thenReturn(productEntity) + + productService.send(guid, topic) + + verify(productRepository, times(1)).findByGuid(guid) + verify(productProducer, times(1)).produceProductSync(producingProductDto) + } + + @Test + fun `send - not found`() { + val topic = "some-topic" + + whenever(productRepository.findByGuid(any())).thenReturn(null) + + assertThrows { + productService.send(guid, topic) + } + + verify(productRepository, times(1)).findByGuid(guid) + verifyNoInteractions(productProducer) + } } diff --git a/src/main/kotlin/com/github/dannecron/demo/http/controllers/ProductController.kt b/src/main/kotlin/com/github/dannecron/demo/http/controllers/ProductController.kt index c48cf41..b27d18f 100644 --- a/src/main/kotlin/com/github/dannecron/demo/http/controllers/ProductController.kt +++ b/src/main/kotlin/com/github/dannecron/demo/http/controllers/ProductController.kt @@ -2,6 +2,7 @@ package com.github.dannecron.demo.http.controllers import com.github.dannecron.demo.core.dto.Product import com.github.dannecron.demo.core.exceptions.AlreadyDeletedException +import com.github.dannecron.demo.core.exceptions.InvalidDataException import com.github.dannecron.demo.core.exceptions.ProductNotFoundException import com.github.dannecron.demo.core.services.product.ProductService import com.github.dannecron.demo.http.exceptions.NotFoundException @@ -10,8 +11,6 @@ import com.github.dannecron.demo.http.requests.CreateProductRequest import com.github.dannecron.demo.http.responses.NotFoundResponse import com.github.dannecron.demo.http.responses.makeOkResponse import com.github.dannecron.demo.http.responses.page.PageResponse -import com.github.dannecron.demo.services.ProductSyncService -import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse @@ -29,7 +28,6 @@ import java.util.* @RequestMapping(value = ["/api/product"], produces = [MediaType.APPLICATION_JSON_VALUE]) class ProductController( private val productService: ProductService, - private val productSyncService: ProductSyncService, ) { @GetMapping("/{guid}") @Throws(NotFoundException::class) @@ -73,8 +71,8 @@ class ProductController( @RequestParam(required = false) topic: String? ): ResponseEntity { try { - productSyncService.syncToKafka(guid, topic) - } catch (_: InvalidArgumentException) { + productService.send(guid, topic) + } catch (_: InvalidDataException) { throw UnprocessableException("cannot sync product to kafka") } diff --git a/src/test/kotlin/com/github/dannecron/demo/http/controllers/ProductControllerTest.kt b/src/test/kotlin/com/github/dannecron/demo/http/controllers/ProductControllerTest.kt index 7ded05b..d21b2cc 100644 --- a/src/test/kotlin/com/github/dannecron/demo/http/controllers/ProductControllerTest.kt +++ b/src/test/kotlin/com/github/dannecron/demo/http/controllers/ProductControllerTest.kt @@ -4,7 +4,6 @@ import com.github.dannecron.demo.BaseUnitTest import com.github.dannecron.demo.core.dto.Product import com.github.dannecron.demo.core.services.product.ProductService import com.github.dannecron.demo.http.responses.ResponseStatus -import com.github.dannecron.demo.services.ProductSyncService import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.nullValue import org.junit.jupiter.api.Test @@ -39,9 +38,6 @@ class ProductControllerTest: BaseUnitTest() { @MockBean private lateinit var productService: ProductService - @MockBean - private lateinit var productSyncService: ProductSyncService - private val guid = UUID.randomUUID() private val now = OffsetDateTime.now() private val productId = 12L @@ -72,7 +68,6 @@ class ProductControllerTest: BaseUnitTest() { .andExpect { jsonPath("\$.updatedAt") { value(nullValue()) } } verify(productService, times(1)).findByGuid(guid) - verifyNoInteractions(productSyncService) } @Test @@ -85,7 +80,6 @@ class ProductControllerTest: BaseUnitTest() { .andExpect { jsonPath("\$.status") { value(ResponseStatus.NOT_FOUND.status) } } verify(productService, times(1)).findByGuid(guid) - verifyNoInteractions(productSyncService) } @Test @@ -109,7 +103,6 @@ class ProductControllerTest: BaseUnitTest() { .andExpect { jsonPath("\$.data[0].isDeleted") { value(false) } } verify(productService, times(1)).findAll(pageRequest) - verifyNoInteractions(productSyncService) } @Test @@ -127,7 +120,6 @@ class ProductControllerTest: BaseUnitTest() { .andExpect { jsonPath("\$.id") { value(productId) } } verify(productService, times(1)).create(productName, productPrice, null) - verifyNoInteractions(productSyncService) } @Test @@ -144,7 +136,6 @@ class ProductControllerTest: BaseUnitTest() { .andExpect { jsonPath("\$.cause") { contains("name") } } verifyNoInteractions(productService) - verifyNoInteractions(productSyncService) } @Test @@ -161,7 +152,6 @@ class ProductControllerTest: BaseUnitTest() { .andExpect { jsonPath("\$.cause") { value(MethodArgumentNotValidException::class.qualifiedName) } } verifyNoInteractions(productService) - verifyNoInteractions(productSyncService) } @Test @@ -176,6 +166,5 @@ class ProductControllerTest: BaseUnitTest() { .andExpect { jsonPath("\$.status") { value(ResponseStatus.OK.status) } } verify(productService, times(1)).delete(guid) - verifyNoInteractions(productSyncService) } }