mirror of
https://github.com/Dannecron/spring-boot-demo.git
synced 2025-12-25 16:22:35 +03:00
main app refactoring
This commit is contained in:
@@ -44,18 +44,19 @@ allprojects {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(rootProject.libs.kotlin.reflect)
|
implementation(rootProject.libs.kotlin.reflect)
|
||||||
implementation(rootProject.libs.kotlinx.serialization.json)
|
implementation(rootProject.libs.kotlinx.serialization.json)
|
||||||
|
implementation(rootProject.libs.logback.encoder)
|
||||||
implementation(rootProject.libs.spring.aspects)
|
implementation(rootProject.libs.spring.aspects)
|
||||||
|
|
||||||
testImplementation(rootProject.libs.kotlin.test.junit)
|
testImplementation(rootProject.libs.kotlin.test.junit)
|
||||||
testImplementation(rootProject.libs.mockito.kotlin)
|
testImplementation(rootProject.libs.mockito.kotlin)
|
||||||
testImplementation(rootProject.libs.spring.boot.starter.test)
|
testImplementation(rootProject.libs.spring.boot.starter.test)
|
||||||
|
|
||||||
kover(project(":db"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.test {
|
tasks.test {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
finalizedBy("koverXmlReport")
|
finalizedBy("koverXmlReport")
|
||||||
|
|
||||||
|
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/java.util=ALL-UNNAMED")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +79,6 @@ dependencies {
|
|||||||
implementation(libs.jackson.module.kotlin)
|
implementation(libs.jackson.module.kotlin)
|
||||||
implementation(libs.ktor.client.cio)
|
implementation(libs.ktor.client.cio)
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.logback.encoder)
|
|
||||||
implementation(libs.postgres)
|
implementation(libs.postgres)
|
||||||
implementation(libs.spring.boot.starter.actuator)
|
implementation(libs.spring.boot.starter.actuator)
|
||||||
implementation(libs.spring.boot.starter.jdbc)
|
implementation(libs.spring.boot.starter.jdbc)
|
||||||
@@ -95,4 +95,7 @@ dependencies {
|
|||||||
testImplementation(libs.testcontainers.junit.jupiter)
|
testImplementation(libs.testcontainers.junit.jupiter)
|
||||||
|
|
||||||
developmentOnly(libs.spring.boot.devtools)
|
developmentOnly(libs.spring.boot.devtools)
|
||||||
|
|
||||||
|
kover(project(":core"))
|
||||||
|
kover(project(":db"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,12 @@ package com.github.dannecron.demo.config
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature
|
import com.fasterxml.jackson.databind.SerializationFeature
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||||
import com.github.dannecron.demo.config.properties.KafkaProperties
|
import com.github.dannecron.demo.core.config.properties.ValidationProperties
|
||||||
import com.github.dannecron.demo.config.properties.ValidationProperties
|
import io.ktor.client.engine.HttpClientEngine
|
||||||
import com.github.dannecron.demo.providers.CityRepository
|
import io.ktor.client.engine.cio.CIO
|
||||||
import com.github.dannecron.demo.providers.CustomerRepository
|
|
||||||
import com.github.dannecron.demo.providers.ProductRepository
|
|
||||||
import com.github.dannecron.demo.services.database.city.CityService
|
|
||||||
import com.github.dannecron.demo.services.database.city.CityServiceImpl
|
|
||||||
import com.github.dannecron.demo.services.database.customer.CustomerService
|
|
||||||
import com.github.dannecron.demo.services.database.customer.CustomerServiceImpl
|
|
||||||
import com.github.dannecron.demo.services.database.product.ProductService
|
|
||||||
import com.github.dannecron.demo.services.database.product.ProductServiceImpl
|
|
||||||
import com.github.dannecron.demo.services.kafka.Producer
|
|
||||||
import com.github.dannecron.demo.services.validation.SchemaValidator
|
|
||||||
import com.github.dannecron.demo.services.validation.SchemaValidatorImp
|
|
||||||
import io.ktor.client.engine.*
|
|
||||||
import io.ktor.client.engine.cio.*
|
|
||||||
import io.micrometer.observation.ObservationRegistry
|
import io.micrometer.observation.ObservationRegistry
|
||||||
import io.micrometer.observation.aop.ObservedAspect
|
import io.micrometer.observation.aop.ObservedAspect
|
||||||
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
|
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
@@ -31,10 +17,8 @@ import com.github.dannecron.demo.services.neko.Client as NekoClient
|
|||||||
import com.github.dannecron.demo.services.neko.ClientImpl as NekoClientImpl
|
import com.github.dannecron.demo.services.neko.ClientImpl as NekoClientImpl
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableConfigurationProperties(KafkaProperties::class, ValidationProperties::class)
|
@EnableConfigurationProperties(ValidationProperties::class)
|
||||||
class AppConfig(
|
class AppConfig {
|
||||||
@Autowired private val kafkaProperties: KafkaProperties,
|
|
||||||
) {
|
|
||||||
@Bean
|
@Bean
|
||||||
fun objectMapper(): ObjectMapper = ObjectMapper().apply {
|
fun objectMapper(): ObjectMapper = ObjectMapper().apply {
|
||||||
registerModules(JavaTimeModule())
|
registerModules(JavaTimeModule())
|
||||||
@@ -42,43 +26,20 @@ class AppConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun productService(
|
fun otlpHttpSpanExporter(@Value("\${tracing.url}") url: String): OtlpHttpSpanExporter =
|
||||||
@Autowired productRepository: ProductRepository,
|
OtlpHttpSpanExporter.builder()
|
||||||
@Autowired producer: Producer,
|
.setEndpoint(url)
|
||||||
): ProductService = ProductServiceImpl(
|
.build()
|
||||||
kafkaProperties.producer.product.defaultSyncTopic,
|
|
||||||
productRepository,
|
|
||||||
producer,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun cityService(@Autowired cityRepository: CityRepository): CityService = CityServiceImpl(cityRepository)
|
fun observedAspect(observationRegistry: ObservationRegistry) = ObservedAspect(observationRegistry)
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun customerService(
|
|
||||||
@Autowired customerRepository: CustomerRepository,
|
|
||||||
@Autowired cityRepository: CityRepository,
|
|
||||||
): CustomerService = CustomerServiceImpl(customerRepository, cityRepository)
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun schemaValidator(
|
|
||||||
@Autowired validationProperties: ValidationProperties,
|
|
||||||
): SchemaValidator = SchemaValidatorImp(validationProperties.schema)
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun otlpHttpSpanExporter(@Value("\${tracing.url}") url: String) = OtlpHttpSpanExporter.builder()
|
|
||||||
.setEndpoint(url)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun observedAspect(@Autowired observationRegistry: ObservationRegistry) = ObservedAspect(observationRegistry)
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun httpClientEngine(): HttpClientEngine = CIO.create()
|
fun httpClientEngine(): HttpClientEngine = CIO.create()
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun nekoClient(
|
fun nekoClient(
|
||||||
@Autowired httpClientEngine: HttpClientEngine,
|
httpClientEngine: HttpClientEngine,
|
||||||
@Value("\${neko.baseUrl}") baseUrl: String,
|
@Value("\${neko.baseUrl}") baseUrl: String,
|
||||||
): NekoClient = NekoClientImpl(
|
): NekoClient = NekoClientImpl(
|
||||||
engine = httpClientEngine,
|
engine = httpClientEngine,
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
package com.github.dannecron.demo.config
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.config.properties.KafkaProperties
|
|
||||||
import com.github.dannecron.demo.services.database.city.CityService
|
|
||||||
import com.github.dannecron.demo.services.kafka.Consumer
|
|
||||||
import io.micrometer.core.instrument.MeterRegistry
|
|
||||||
import org.apache.kafka.clients.consumer.ConsumerConfig
|
|
||||||
import org.apache.kafka.common.serialization.StringDeserializer
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.context.annotation.Bean
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory
|
|
||||||
import org.springframework.kafka.core.ConsumerFactory
|
|
||||||
import org.springframework.kafka.core.DefaultKafkaConsumerFactory
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
class KafkaConsumerConfig(
|
|
||||||
@Autowired val kafkaProperties: KafkaProperties
|
|
||||||
) {
|
|
||||||
@Bean
|
|
||||||
fun consumer(
|
|
||||||
@Autowired cityService: CityService,
|
|
||||||
@Autowired metricRegistry: MeterRegistry
|
|
||||||
): Consumer = Consumer(
|
|
||||||
cityService = cityService,
|
|
||||||
metricRegistry = metricRegistry,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun consumerFactory(): ConsumerFactory<String, String> = DefaultKafkaConsumerFactory(mapOf(
|
|
||||||
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to kafkaProperties.bootstrapServers,
|
|
||||||
ConsumerConfig.GROUP_ID_CONFIG to kafkaProperties.consumer.groupId,
|
|
||||||
ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to kafkaProperties.consumer.autoOffsetReset,
|
|
||||||
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,
|
|
||||||
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java
|
|
||||||
))
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun kafkaListenerContainerFactory() = ConcurrentKafkaListenerContainerFactory<String, String>().apply {
|
|
||||||
consumerFactory = consumerFactory()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.github.dannecron.demo.config
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.config.properties.KafkaProperties
|
|
||||||
import com.github.dannecron.demo.services.kafka.Producer
|
|
||||||
import com.github.dannecron.demo.services.kafka.ProducerImpl
|
|
||||||
import com.github.dannecron.demo.services.validation.SchemaValidator
|
|
||||||
import org.apache.kafka.clients.producer.ProducerConfig
|
|
||||||
import org.apache.kafka.common.serialization.StringSerializer
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.context.annotation.Bean
|
|
||||||
import org.springframework.context.annotation.Configuration
|
|
||||||
import org.springframework.kafka.core.DefaultKafkaProducerFactory
|
|
||||||
import org.springframework.kafka.core.KafkaTemplate
|
|
||||||
import org.springframework.kafka.core.ProducerFactory
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
class KafkaProducerConfig(
|
|
||||||
@Autowired val kafkaProperties: KafkaProperties
|
|
||||||
) {
|
|
||||||
@Bean
|
|
||||||
fun producerFactory(): ProducerFactory<String, Any> = DefaultKafkaProducerFactory(mapOf(
|
|
||||||
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to kafkaProperties.bootstrapServers,
|
|
||||||
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
|
|
||||||
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,
|
|
||||||
))
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun kafkaTemplate(): KafkaTemplate<String, Any> = KafkaTemplate(
|
|
||||||
producerFactory(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun producer(
|
|
||||||
@Autowired kafkaTemplate: KafkaTemplate<String, Any>,
|
|
||||||
@Autowired schemaValidator: SchemaValidator,
|
|
||||||
): Producer = ProducerImpl(
|
|
||||||
kafkaTemplate,
|
|
||||||
schemaValidator,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.github.dannecron.demo.config.kafka
|
||||||
|
|
||||||
|
import com.github.dannecron.demo.services.kafka.CityCreateConsumer
|
||||||
|
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import java.util.function.Consumer
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class CityConsumerConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun citySyncConsumer(cityCreateConsumer: CityCreateConsumer): Consumer<CityCreateDto> =
|
||||||
|
Consumer(cityCreateConsumer::process)
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.github.dannecron.demo.config.properties
|
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
|
||||||
import org.springframework.boot.context.properties.bind.ConstructorBinding
|
|
||||||
|
|
||||||
@ConfigurationProperties("kafka")
|
|
||||||
data class KafkaProperties @ConstructorBinding constructor(
|
|
||||||
val bootstrapServers: String,
|
|
||||||
val producer: Producer,
|
|
||||||
val consumer: Consumer,
|
|
||||||
) {
|
|
||||||
data class Producer(
|
|
||||||
val product: Product,
|
|
||||||
) {
|
|
||||||
data class Product(
|
|
||||||
val defaultSyncTopic: String
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Consumer(
|
|
||||||
val groupId: String,
|
|
||||||
val topics: String,
|
|
||||||
val autoStartup: Boolean,
|
|
||||||
val autoOffsetReset: String,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.github.dannecron.demo.http.controllers
|
package com.github.dannecron.demo.http.controllers
|
||||||
|
|
||||||
|
import com.github.dannecron.demo.core.services.customer.CustomerService
|
||||||
import com.github.dannecron.demo.http.exceptions.NotFoundException
|
import com.github.dannecron.demo.http.exceptions.NotFoundException
|
||||||
import com.github.dannecron.demo.services.database.customer.CustomerService
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.http.HttpStatus
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package com.github.dannecron.demo.http.controllers
|
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.ProductNotFoundException
|
||||||
|
import com.github.dannecron.demo.core.services.product.ProductService
|
||||||
import com.github.dannecron.demo.http.exceptions.NotFoundException
|
import com.github.dannecron.demo.http.exceptions.NotFoundException
|
||||||
import com.github.dannecron.demo.http.exceptions.UnprocessableException
|
import com.github.dannecron.demo.http.exceptions.UnprocessableException
|
||||||
import com.github.dannecron.demo.http.requests.CreateProductRequest
|
import com.github.dannecron.demo.http.requests.CreateProductRequest
|
||||||
import com.github.dannecron.demo.http.responses.NotFoundResponse
|
import com.github.dannecron.demo.http.responses.NotFoundResponse
|
||||||
import com.github.dannecron.demo.http.responses.makeOkResponse
|
import com.github.dannecron.demo.http.responses.makeOkResponse
|
||||||
import com.github.dannecron.demo.http.responses.page.PageResponse
|
import com.github.dannecron.demo.http.responses.page.PageResponse
|
||||||
import com.github.dannecron.demo.models.Product
|
import com.github.dannecron.demo.services.ProductSyncService
|
||||||
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
|
|
||||||
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
|
|
||||||
import com.github.dannecron.demo.services.database.product.ProductService
|
|
||||||
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
|
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
|
||||||
import io.swagger.v3.oas.annotations.media.Content
|
import io.swagger.v3.oas.annotations.media.Content
|
||||||
import io.swagger.v3.oas.annotations.media.Schema
|
import io.swagger.v3.oas.annotations.media.Schema
|
||||||
@@ -27,7 +28,8 @@ import java.util.*
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(value = ["/api/product"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
@RequestMapping(value = ["/api/product"], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
class ProductController(
|
class ProductController(
|
||||||
val productService: ProductService,
|
private val productService: ProductService,
|
||||||
|
private val productSyncService: ProductSyncService,
|
||||||
) {
|
) {
|
||||||
@GetMapping("/{guid}")
|
@GetMapping("/{guid}")
|
||||||
@Throws(NotFoundException::class)
|
@Throws(NotFoundException::class)
|
||||||
@@ -71,7 +73,7 @@ class ProductController(
|
|||||||
@RequestParam(required = false) topic: String?
|
@RequestParam(required = false) topic: String?
|
||||||
): ResponseEntity<Any> {
|
): ResponseEntity<Any> {
|
||||||
try {
|
try {
|
||||||
productService.syncToKafka(guid, topic)
|
productSyncService.syncToKafka(guid, topic)
|
||||||
} catch (_: InvalidArgumentException) {
|
} catch (_: InvalidArgumentException) {
|
||||||
throw UnprocessableException("cannot sync product to kafka")
|
throw UnprocessableException("cannot sync product to kafka")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.github.dannecron.demo.models
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.db.entity.City
|
|
||||||
import com.github.dannecron.demo.db.entity.Customer
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CustomerExtended(
|
|
||||||
val customer: Customer,
|
|
||||||
val city: City?,
|
|
||||||
)
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.github.dannecron.demo.models
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.db.entity.Product
|
|
||||||
import com.github.dannecron.demo.db.entity.order.Order
|
|
||||||
|
|
||||||
data class OrderWithProducts(
|
|
||||||
val order: Order,
|
|
||||||
val products: List<Product>,
|
|
||||||
) {
|
|
||||||
fun getMostExpensiveOrderedProduct(): Product? = products.maxByOrNull { pr -> pr.price }
|
|
||||||
|
|
||||||
fun getTotalOrderPrice(): Double = products.sumOf { pr -> pr.getPriceDouble() }
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.github.dannecron.demo.providers
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.models.City
|
|
||||||
import org.springframework.data.jdbc.repository.query.Query
|
|
||||||
import org.springframework.data.repository.CrudRepository
|
|
||||||
import org.springframework.data.repository.query.Param
|
|
||||||
import org.springframework.stereotype.Repository
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
interface CityRepository: CrudRepository<City, Long> {
|
|
||||||
fun findByGuid(guid: UUID): City?
|
|
||||||
|
|
||||||
@Query(value = "UPDATE City SET deleted_at = :deletedAt WHERE guid = :guid RETURNING *")
|
|
||||||
fun softDelete(@Param("guid") guid: UUID, @Param("deletedAt") deletedAt: OffsetDateTime): City?
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.github.dannecron.demo.providers
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.models.Customer
|
|
||||||
import org.springframework.data.repository.CrudRepository
|
|
||||||
import org.springframework.stereotype.Repository
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
interface CustomerRepository: CrudRepository<Customer, Long> {
|
|
||||||
fun findByGuid(guid: UUID): Customer?
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.github.dannecron.demo.providers
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.models.order.OrderProduct
|
|
||||||
import org.springframework.data.repository.CrudRepository
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
interface OrderProductRepository: CrudRepository<OrderProduct, UUID> {
|
|
||||||
fun findByOrderId(orderId: Long): List<OrderProduct>
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.github.dannecron.demo.providers
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.models.order.Order
|
|
||||||
import org.springframework.data.repository.CrudRepository
|
|
||||||
import org.springframework.stereotype.Repository
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
interface OrderRepository: CrudRepository<Order, Long> {
|
|
||||||
fun findByCustomerId(customerId: Long): List<Order>
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.github.dannecron.demo.providers
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.models.Product
|
|
||||||
import org.springframework.data.jdbc.repository.query.Query
|
|
||||||
import org.springframework.data.repository.CrudRepository
|
|
||||||
import org.springframework.data.repository.PagingAndSortingRepository
|
|
||||||
import org.springframework.data.repository.query.Param
|
|
||||||
import org.springframework.stereotype.Repository
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
interface ProductRepository: CrudRepository<Product, Long>, PagingAndSortingRepository<Product, Long> {
|
|
||||||
fun findByGuid(guid: UUID): Product?
|
|
||||||
|
|
||||||
@Query(value = "UPDATE Product SET deleted_at = :deletedAt WHERE guid = :guid RETURNING *")
|
|
||||||
fun softDelete(@Param("guid") guid: UUID, @Param("deletedAt") deletedAt: OffsetDateTime): Product?
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
package com.github.dannecron.demo.providers.html
|
package com.github.dannecron.demo.providers.html
|
||||||
|
|
||||||
class Html: com.github.dannecron.demo.providers.html.Tag("html")
|
class Html: Tag("html")
|
||||||
|
|
||||||
fun html(init: com.github.dannecron.demo.providers.html.Html.() -> Unit): com.github.dannecron.demo.providers.html.Html {
|
fun html(init: Html.() -> Unit): Html {
|
||||||
val tag = com.github.dannecron.demo.providers.html.Html()
|
val tag = Html()
|
||||||
tag.init()
|
tag.init()
|
||||||
return tag
|
return tag
|
||||||
}
|
}
|
||||||
|
|
||||||
fun com.github.dannecron.demo.providers.html.Html.table(init : com.github.dannecron.demo.providers.html.Table.() -> Unit) = doInit(
|
fun Html.table(init : Table.() -> Unit) = doInit(
|
||||||
com.github.dannecron.demo.providers.html.Table(), init)
|
Table(), init)
|
||||||
fun com.github.dannecron.demo.providers.html.Html.center(init : com.github.dannecron.demo.providers.html.Center.() -> Unit) = doInit(
|
fun Html.center(init : Center.() -> Unit) = doInit(
|
||||||
com.github.dannecron.demo.providers.html.Center(), init)
|
Center(), init)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ fun getTitleColor() = "#b9c9fe"
|
|||||||
fun getCellColor(index: Int, row: Int) = if ((index + row) %2 == 0) "#dce4ff" else "#eff2ff"
|
fun getCellColor(index: Int, row: Int) = if ((index + row) %2 == 0) "#dce4ff" else "#eff2ff"
|
||||||
|
|
||||||
fun renderProductTable(): String {
|
fun renderProductTable(): String {
|
||||||
return com.github.dannecron.demo.providers.html.html {
|
return html {
|
||||||
table {
|
table {
|
||||||
tr(color = getTitleColor()) {
|
tr(color = getTitleColor()) {
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.github.dannecron.demo.providers.html
|
package com.github.dannecron.demo.providers.html
|
||||||
|
|
||||||
open class Tag(val name: String) {
|
open class Tag(val name: String) {
|
||||||
val children: MutableList<Tag> = ArrayList()
|
val children: MutableList<Tag> = mutableListOf()
|
||||||
val attributes: MutableList<Attribute> = ArrayList()
|
val attributes: MutableList<Attribute> = mutableListOf()
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "<$name" +
|
return "<$name" +
|
||||||
@@ -25,4 +25,4 @@ fun <T: Tag> Tag.doInit(tag: T, init: T.() -> Unit): T {
|
|||||||
return tag
|
return tag
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Tag.text(s : Any?) = doInit(Text(s.toString()), {})
|
fun Tag.text(s : Any?) = doInit(Text(s.toString())) {}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.github.dannecron.demo.services
|
||||||
|
|
||||||
|
|
||||||
|
import com.github.dannecron.demo.core.exceptions.ProductNotFoundException
|
||||||
|
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
interface ProductSyncService {
|
||||||
|
|
||||||
|
@Throws(ProductNotFoundException::class, InvalidArgumentException::class)
|
||||||
|
fun syncToKafka(guid: UUID, topic: String?)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.github.dannecron.demo.services
|
||||||
|
|
||||||
|
import com.github.dannecron.demo.core.dto.Product
|
||||||
|
import com.github.dannecron.demo.core.exceptions.ProductNotFoundException
|
||||||
|
import com.github.dannecron.demo.core.services.product.ProductService
|
||||||
|
import com.github.dannecron.demo.services.kafka.Producer
|
||||||
|
import com.github.dannecron.demo.services.kafka.dto.ProductDto
|
||||||
|
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class ProductSyncServiceImpl(
|
||||||
|
private val productService: ProductService,
|
||||||
|
private val producer: Producer,
|
||||||
|
) : ProductSyncService {
|
||||||
|
|
||||||
|
@Throws(ProductNotFoundException::class, InvalidArgumentException::class)
|
||||||
|
override fun syncToKafka(guid: UUID, topic: String?) {
|
||||||
|
val product = productService.findByGuid(guid) ?: throw ProductNotFoundException()
|
||||||
|
|
||||||
|
producer.produceProductSync(product.toKafkaDto())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Product.toKafkaDto() = 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),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.github.dannecron.demo.services.database.customer
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.db.entity.Customer
|
|
||||||
import com.github.dannecron.demo.models.CustomerExtended
|
|
||||||
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
interface CustomerService {
|
|
||||||
fun findByGuid(guid: UUID): CustomerExtended?
|
|
||||||
|
|
||||||
@Throws(CityNotFoundException::class)
|
|
||||||
fun create(name: String, cityGuid: UUID?): Customer
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package com.github.dannecron.demo.services.database.customer
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
|
||||||
import com.github.dannecron.demo.db.entity.Customer
|
|
||||||
import com.github.dannecron.demo.db.repository.CityRepository
|
|
||||||
import com.github.dannecron.demo.db.repository.CustomerRepository
|
|
||||||
import com.github.dannecron.demo.models.CustomerExtended
|
|
||||||
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class CustomerServiceImpl(
|
|
||||||
private val customerRepository: CustomerRepository,
|
|
||||||
private val cityRepository: CityRepository,
|
|
||||||
private val commonGenerator: CommonGenerator,
|
|
||||||
): CustomerService {
|
|
||||||
override fun findByGuid(guid: UUID): CustomerExtended? = customerRepository.findByGuid(guid)
|
|
||||||
?.let {
|
|
||||||
customer -> CustomerExtended(
|
|
||||||
customer = customer,
|
|
||||||
city = customer.cityId?.let { cityId -> cityRepository.findById(cityId).orElse(null) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun create(name: String, cityGuid: UUID?): Customer = Customer(
|
|
||||||
id = null,
|
|
||||||
guid = commonGenerator.generateUUID(),
|
|
||||||
name = name,
|
|
||||||
cityId = cityGuid?.let {
|
|
||||||
cityRepository.findByGuid(it)?.id ?: throw CityNotFoundException()
|
|
||||||
},
|
|
||||||
createdAt = commonGenerator.generateCurrentTime(),
|
|
||||||
updatedAt = null,
|
|
||||||
).let {
|
|
||||||
customerRepository.save(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package com.github.dannecron.demo.services.database.exceptions
|
|
||||||
|
|
||||||
class AlreadyDeletedException: RuntimeException()
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package com.github.dannecron.demo.services.database.exceptions
|
|
||||||
|
|
||||||
class CityNotFoundException: ModelNotFoundException("city")
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package com.github.dannecron.demo.services.database.exceptions
|
|
||||||
|
|
||||||
open class ModelNotFoundException(entityName: String): RuntimeException("$entityName not found")
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package com.github.dannecron.demo.services.database.exceptions
|
|
||||||
|
|
||||||
class ProductNotFoundException: ModelNotFoundException("product")
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package com.github.dannecron.demo.services.database.order
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
|
||||||
import com.github.dannecron.demo.db.entity.Customer
|
|
||||||
import com.github.dannecron.demo.db.entity.Product
|
|
||||||
import com.github.dannecron.demo.db.entity.order.Order
|
|
||||||
import com.github.dannecron.demo.db.entity.order.OrderProduct
|
|
||||||
import com.github.dannecron.demo.db.repository.OrderProductRepository
|
|
||||||
import com.github.dannecron.demo.db.repository.OrderRepository
|
|
||||||
import com.github.dannecron.demo.db.repository.ProductRepository
|
|
||||||
import com.github.dannecron.demo.models.OrderWithProducts
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import org.springframework.transaction.annotation.Transactional
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class OrderServiceImpl(
|
|
||||||
private val orderRepository: OrderRepository,
|
|
||||||
private val orderProductRepository: OrderProductRepository,
|
|
||||||
private val productRepository: ProductRepository,
|
|
||||||
private val commonGenerator: CommonGenerator,
|
|
||||||
) {
|
|
||||||
fun findByCustomerId(customerId: Long): List<OrderWithProducts> = orderRepository.findByCustomerId(customerId)
|
|
||||||
.let { orders -> orders.map { order -> OrderWithProducts(
|
|
||||||
order = order,
|
|
||||||
products = findProductsByOrderId(order.id!!),
|
|
||||||
) } }
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
fun createOrder(customer: Customer, products: Set<Product>): Order {
|
|
||||||
val order = Order(
|
|
||||||
id = null,
|
|
||||||
guid = commonGenerator.generateUUID(),
|
|
||||||
customerId = customer.id!!,
|
|
||||||
deliveredAt = null,
|
|
||||||
createdAt = commonGenerator.generateCurrentTime(),
|
|
||||||
updatedAt = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
return orderRepository.save(order)
|
|
||||||
.also { saveProductsForNewOrder(it, products.toList()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findProductsByOrderId(orderId: Long): List<Product> =
|
|
||||||
orderProductRepository.findByOrderId(orderId = orderId)
|
|
||||||
.map { it.productId }
|
|
||||||
.let {
|
|
||||||
if (it.isEmpty()) {
|
|
||||||
emptyList()
|
|
||||||
} else {
|
|
||||||
productRepository.findAllById(it).toList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveProductsForNewOrder(savedOrder: Order, products: List<Product>) {
|
|
||||||
products.map {
|
|
||||||
OrderProduct(
|
|
||||||
guid = commonGenerator.generateUUID(),
|
|
||||||
orderId = savedOrder.id!!,
|
|
||||||
productId = it.id!!,
|
|
||||||
createdAt = commonGenerator.generateCurrentTime(),
|
|
||||||
updatedAt = null
|
|
||||||
)
|
|
||||||
}.also { orderProductRepository.saveAll(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.github.dannecron.demo.services.kafka
|
||||||
|
|
||||||
|
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
||||||
|
|
||||||
|
interface CityCreateConsumer {
|
||||||
|
fun process(cityCreateDto: CityCreateDto)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.github.dannecron.demo.services.kafka
|
||||||
|
|
||||||
|
import com.github.dannecron.demo.core.dto.CityCreate
|
||||||
|
import com.github.dannecron.demo.core.services.city.CityService
|
||||||
|
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
||||||
|
import com.github.dannecron.demo.services.metrics.MetricsSender
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class CityCreateConsumerImpl(
|
||||||
|
private val cityService: CityService,
|
||||||
|
private val metricsSender: MetricsSender,
|
||||||
|
) : CityCreateConsumer {
|
||||||
|
|
||||||
|
override fun process(cityCreateDto: CityCreateDto) {
|
||||||
|
cityService.create(cityCreateDto.toCore()).also {
|
||||||
|
metricsSender.incrementConsumerCityCreate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CityCreateDto.toCore() = CityCreate(
|
||||||
|
guid = guid,
|
||||||
|
name = name,
|
||||||
|
createdAt = OffsetDateTime.parse(createdAt, DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||||
|
updatedAt = updatedAt?.let {
|
||||||
|
OffsetDateTime.parse(it, DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||||
|
},
|
||||||
|
deletedAt = deletedAt?.let {
|
||||||
|
OffsetDateTime.parse(it, DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.github.dannecron.demo.services.kafka
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.services.database.city.CityService
|
|
||||||
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
|
||||||
import io.micrometer.core.instrument.Counter
|
|
||||||
import io.micrometer.core.instrument.MeterRegistry
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.springframework.kafka.annotation.KafkaListener
|
|
||||||
import org.springframework.messaging.handler.annotation.Payload
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class Consumer(
|
|
||||||
private val cityService: CityService,
|
|
||||||
private val metricRegistry: MeterRegistry,
|
|
||||||
) {
|
|
||||||
@KafkaListener(
|
|
||||||
topics = ["#{'\${kafka.consumer.topics}'.split(',')}"],
|
|
||||||
autoStartup = "\${kafka.consumer.auto-startup:false}",
|
|
||||||
)
|
|
||||||
fun handleCityCreate(@Payload message: String) {
|
|
||||||
val cityCreateDto = Json.decodeFromString<CityCreateDto>(message)
|
|
||||||
.also {
|
|
||||||
val counter = Counter.builder("kafka_consumer_city_create")
|
|
||||||
.description("consumed created city event")
|
|
||||||
.register(metricRegistry)
|
|
||||||
counter.increment()
|
|
||||||
}
|
|
||||||
|
|
||||||
cityService.create(cityCreateDto)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.github.dannecron.demo.services.kafka
|
package com.github.dannecron.demo.services.kafka
|
||||||
|
|
||||||
import com.github.dannecron.demo.models.Product
|
import com.github.dannecron.demo.services.kafka.dto.ProductDto
|
||||||
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
|
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
|
||||||
|
|
||||||
interface Producer {
|
interface Producer {
|
||||||
@Throws(InvalidArgumentException::class)
|
@Throws(InvalidArgumentException::class)
|
||||||
fun produceProductInfo(topicName: String, product: Product)
|
fun produceProductSync(product: ProductDto)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
package com.github.dannecron.demo.services.kafka
|
package com.github.dannecron.demo.services.kafka
|
||||||
|
|
||||||
import com.github.dannecron.demo.models.Product
|
import com.github.dannecron.demo.core.services.validation.SchemaValidator
|
||||||
import com.github.dannecron.demo.services.kafka.dto.ProductDto
|
import com.github.dannecron.demo.services.kafka.dto.ProductDto
|
||||||
import com.github.dannecron.demo.services.validation.SchemaValidator
|
|
||||||
import com.github.dannecron.demo.services.validation.SchemaValidator.Companion.SCHEMA_KAFKA_PRODUCT_SYNC
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.encodeToJsonElement
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
import org.springframework.kafka.core.KafkaTemplate
|
import org.springframework.cloud.stream.function.StreamBridge
|
||||||
import org.springframework.kafka.support.KafkaHeaders
|
|
||||||
import org.springframework.messaging.support.MessageBuilder
|
import org.springframework.messaging.support.MessageBuilder
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class ProducerImpl(
|
class ProducerImpl(
|
||||||
private val kafkaTemplate: KafkaTemplate<String, Any>,
|
private val streamBridge: StreamBridge,
|
||||||
private val schemaValidator: SchemaValidator,
|
private val schemaValidator: SchemaValidator,
|
||||||
): Producer {
|
): Producer {
|
||||||
override fun produceProductInfo(topicName: String, product: Product) {
|
private companion object {
|
||||||
Json.encodeToJsonElement(ProductDto(product)).let {
|
private const val BINDING_NAME_PRODUCT_SYNC = "productSyncProducer"
|
||||||
schemaValidator.validate(SCHEMA_KAFKA_PRODUCT_SYNC, it)
|
private const val SCHEMA_KAFKA_PRODUCT_SYNC = "kafka-product-sync"
|
||||||
|
}
|
||||||
|
|
||||||
MessageBuilder.withPayload(it.toString())
|
override fun produceProductSync(product: ProductDto) {
|
||||||
.setHeader(KafkaHeaders.TOPIC, topicName)
|
Json.encodeToJsonElement((product))
|
||||||
.setHeader("X-Custom-Header", "some-custom-header")
|
.also { schemaValidator.validate(SCHEMA_KAFKA_PRODUCT_SYNC, it) }
|
||||||
.build()
|
|
||||||
}
|
|
||||||
.let {
|
.let {
|
||||||
msg -> kafkaTemplate.send(msg)
|
MessageBuilder.withPayload(it.toString())
|
||||||
|
.setHeader("X-Custom-Header", "some-custom-header")
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
.let {
|
||||||
|
streamBridge.send(
|
||||||
|
BINDING_NAME_PRODUCT_SYNC,
|
||||||
|
it,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package com.github.dannecron.demo.services.kafka.dto
|
package com.github.dannecron.demo.services.kafka.dto
|
||||||
|
|
||||||
import com.github.dannecron.demo.models.Product
|
|
||||||
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ProductDto(
|
data class ProductDto(
|
||||||
@@ -15,16 +12,4 @@ data class ProductDto(
|
|||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String?,
|
val updatedAt: String?,
|
||||||
val deletedAt: String?,
|
val deletedAt: String?,
|
||||||
) {
|
)
|
||||||
@Throws(InvalidArgumentException::class)
|
|
||||||
constructor(product: Product) : this(
|
|
||||||
id = product.id ?: throw InvalidArgumentException("product.id"),
|
|
||||||
guid = product.guid.toString(),
|
|
||||||
name = product.name,
|
|
||||||
description = product.description,
|
|
||||||
price = product.price,
|
|
||||||
createdAt = product.createdAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
|
||||||
updatedAt = product.updatedAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
|
||||||
deletedAt = product.deletedAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.github.dannecron.demo.services.metrics
|
||||||
|
|
||||||
|
interface MetricsSender {
|
||||||
|
fun incrementConsumerCityCreate()
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.github.dannecron.demo.services.metrics
|
||||||
|
|
||||||
|
import io.micrometer.core.instrument.Counter
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class MetricsSenderImpl(
|
||||||
|
metricRegistry: MeterRegistry,
|
||||||
|
) : MetricsSender {
|
||||||
|
private val consumerCityCreateCounter = Counter.builder("kafka_consumer_city_create")
|
||||||
|
.description("consumed created city event")
|
||||||
|
.register(metricRegistry)
|
||||||
|
|
||||||
|
override fun incrementConsumerCityCreate() {
|
||||||
|
consumerCityCreateCounter.increment()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,50 @@ spring:
|
|||||||
default-schema: ${DB_SCHEMA:public}
|
default-schema: ${DB_SCHEMA:public}
|
||||||
profiles:
|
profiles:
|
||||||
active: ${SPRING_ACTIVE_PROFILE:default}
|
active: ${SPRING_ACTIVE_PROFILE:default}
|
||||||
|
kafka:
|
||||||
|
bootstrap-servers: ${KAFKA_SERVERS}
|
||||||
|
security:
|
||||||
|
protocol: PLAINTEXT
|
||||||
|
cloud:
|
||||||
|
discovery:
|
||||||
|
client:
|
||||||
|
composite-indicator:
|
||||||
|
enabled: false
|
||||||
|
function:
|
||||||
|
definition: >
|
||||||
|
citySyncConsumer
|
||||||
|
stream:
|
||||||
|
defaultBinder: kafka
|
||||||
|
kafka:
|
||||||
|
default:
|
||||||
|
producer:
|
||||||
|
sync: true
|
||||||
|
binder:
|
||||||
|
enable-observation: true
|
||||||
|
requiredAcks: all
|
||||||
|
producerProperties:
|
||||||
|
retries: 3
|
||||||
|
key:
|
||||||
|
serializer: org.apache.kafka.common.serialization.StringSerializer
|
||||||
|
consumerProperties:
|
||||||
|
key:
|
||||||
|
deserializer: org.apache.kafka.common.serialization.StringDeserializer
|
||||||
|
bindings:
|
||||||
|
citySyncConsumer-in-0:
|
||||||
|
consumer:
|
||||||
|
enable-dlq: false
|
||||||
|
bindings:
|
||||||
|
# input
|
||||||
|
citySyncConsumer-in-0:
|
||||||
|
group: demo-group
|
||||||
|
destination: demo-city-sync
|
||||||
|
binder: kafka
|
||||||
|
consumer:
|
||||||
|
retry-template-name: DEFAULT
|
||||||
|
# output
|
||||||
|
productSyncProducer:
|
||||||
|
destination: demo-product-sync
|
||||||
|
binder: kafka
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
@@ -26,11 +70,6 @@ kafka:
|
|||||||
producer:
|
producer:
|
||||||
product:
|
product:
|
||||||
default-sync-topic: demo-product-sync
|
default-sync-topic: demo-product-sync
|
||||||
consumer:
|
|
||||||
group-id: demo-consumer
|
|
||||||
topics: demo-city-sync
|
|
||||||
auto-offset-reset: latest
|
|
||||||
auto-startup: true
|
|
||||||
|
|
||||||
validation:
|
validation:
|
||||||
schema:
|
schema:
|
||||||
|
|||||||
@@ -1,38 +1,15 @@
|
|||||||
package com.github.dannecron.demo
|
package com.github.dannecron.demo
|
||||||
|
|
||||||
import com.github.dannecron.demo.config.properties.KafkaProperties
|
import com.github.dannecron.demo.core.config.properties.ValidationProperties
|
||||||
import com.github.dannecron.demo.config.properties.ValidationProperties
|
|
||||||
import com.github.dannecron.demo.services.kafka.Consumer
|
|
||||||
import com.github.dannecron.demo.services.validation.SchemaValidator.Companion.SCHEMA_KAFKA_PRODUCT_SYNC
|
|
||||||
import org.springframework.boot.test.context.TestConfiguration
|
import org.springframework.boot.test.context.TestConfiguration
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean
|
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
|
|
||||||
open class BaseUnitTest {
|
open class BaseUnitTest {
|
||||||
@MockBean
|
|
||||||
lateinit var consumer: Consumer
|
|
||||||
|
|
||||||
@TestConfiguration
|
@TestConfiguration
|
||||||
class TestConfig {
|
class TestConfig {
|
||||||
@Bean
|
|
||||||
fun kafkaProperties(): KafkaProperties = KafkaProperties(
|
|
||||||
bootstrapServers = "localhost:1111",
|
|
||||||
producer = KafkaProperties.Producer(
|
|
||||||
product = KafkaProperties.Producer.Product(
|
|
||||||
defaultSyncTopic = "some-default",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
consumer = KafkaProperties.Consumer(
|
|
||||||
groupId = "group",
|
|
||||||
topics = "topic",
|
|
||||||
autoStartup = false,
|
|
||||||
autoOffsetReset = "none",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun validationProperties(): ValidationProperties = ValidationProperties(
|
fun validationProperties(): ValidationProperties = ValidationProperties(
|
||||||
schema = mapOf(SCHEMA_KAFKA_PRODUCT_SYNC to "kafka/product/sync.json"),
|
schema = mapOf("kafka-product-sync" to "kafka/product/sync.json"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.github.dannecron.demo.config.kafka
|
||||||
|
|
||||||
|
import com.github.dannecron.demo.services.kafka.CityCreateConsumer
|
||||||
|
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.kotlin.after
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||||
|
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
|
||||||
|
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
|
||||||
|
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
|
||||||
|
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
|
||||||
|
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
|
import org.springframework.cloud.stream.binder.test.InputDestination
|
||||||
|
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration
|
||||||
|
import org.springframework.messaging.support.MessageBuilder
|
||||||
|
import org.springframework.test.context.TestPropertySource
|
||||||
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@SpringJUnitConfig(
|
||||||
|
classes = [CityConsumerConfig::class, TestChannelBinderConfiguration::class],
|
||||||
|
)
|
||||||
|
@TestPropertySource(
|
||||||
|
properties = [
|
||||||
|
"spring.jmx.enabled=false",
|
||||||
|
"spring.cloud.stream.default-binder=kafka",
|
||||||
|
"spring.cloud.function.definition=citySyncConsumer",
|
||||||
|
"spring.cloud.stream.bindings.citySyncConsumer-in-0.destination=demo-city-sync"
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@EnableAutoConfiguration(
|
||||||
|
exclude = [
|
||||||
|
WebMvcAutoConfiguration::class,
|
||||||
|
DataSourceAutoConfiguration::class,
|
||||||
|
DataSourceTransactionManagerAutoConfiguration::class,
|
||||||
|
HibernateJpaAutoConfiguration::class,
|
||||||
|
SecurityAutoConfiguration::class,
|
||||||
|
EndpointAutoConfiguration::class,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
class CityEntityCreateConsumerImplConfigTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var inputDestination: InputDestination
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private lateinit var cityCreateConsumer: CityCreateConsumer
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `citySyncConsumer - success`() {
|
||||||
|
val cityGuid = UUID.randomUUID()
|
||||||
|
val cityName = "new-city"
|
||||||
|
val createdAt = OffsetDateTime.now().minusDays(1)
|
||||||
|
val cityCreateDto = CityCreateDto(
|
||||||
|
guid = cityGuid.toString(),
|
||||||
|
name = cityName,
|
||||||
|
createdAt = createdAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||||
|
updatedAt = null,
|
||||||
|
deletedAt = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
val rawEvent = Json.encodeToString(cityCreateDto)
|
||||||
|
val msg = MessageBuilder.withPayload(rawEvent).build()
|
||||||
|
inputDestination.send(msg, "demo-city-sync")
|
||||||
|
|
||||||
|
verify(cityCreateConsumer, after(1000).times(1)).process(cityCreateDto)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.github.dannecron.demo.http.controllers
|
package com.github.dannecron.demo.http.controllers
|
||||||
|
|
||||||
import com.github.dannecron.demo.BaseUnitTest
|
import com.github.dannecron.demo.BaseUnitTest
|
||||||
|
import com.github.dannecron.demo.core.dto.City
|
||||||
|
import com.github.dannecron.demo.core.dto.Customer
|
||||||
|
import com.github.dannecron.demo.core.dto.view.CustomerExtended
|
||||||
|
import com.github.dannecron.demo.core.services.customer.CustomerService
|
||||||
import com.github.dannecron.demo.http.responses.ResponseStatus
|
import com.github.dannecron.demo.http.responses.ResponseStatus
|
||||||
import com.github.dannecron.demo.models.City
|
|
||||||
import com.github.dannecron.demo.models.Customer
|
|
||||||
import com.github.dannecron.demo.models.CustomerExtended
|
|
||||||
import com.github.dannecron.demo.services.database.customer.CustomerService
|
|
||||||
import org.mockito.kotlin.doReturn
|
import org.mockito.kotlin.doReturn
|
||||||
import org.mockito.kotlin.eq
|
import org.mockito.kotlin.eq
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
@@ -16,7 +16,7 @@ import org.springframework.http.MediaType
|
|||||||
import org.springframework.test.web.servlet.MockMvc
|
import org.springframework.test.web.servlet.MockMvc
|
||||||
import org.springframework.test.web.servlet.get
|
import org.springframework.test.web.servlet.get
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
@WebMvcTest(CustomerController::class)
|
@WebMvcTest(CustomerController::class)
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
package com.github.dannecron.demo.http.controllers
|
package com.github.dannecron.demo.http.controllers
|
||||||
|
|
||||||
import com.github.dannecron.demo.BaseUnitTest
|
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.http.responses.ResponseStatus
|
||||||
import com.github.dannecron.demo.models.Product
|
import com.github.dannecron.demo.services.ProductSyncService
|
||||||
import com.github.dannecron.demo.services.database.product.ProductService
|
|
||||||
import org.hamcrest.Matchers.contains
|
import org.hamcrest.Matchers.contains
|
||||||
import org.hamcrest.Matchers.nullValue
|
import org.hamcrest.Matchers.nullValue
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.mockito.kotlin.doReturn
|
import org.mockito.kotlin.any
|
||||||
import org.mockito.kotlin.eq
|
import org.mockito.kotlin.anyOrNull
|
||||||
|
import org.mockito.kotlin.times
|
||||||
|
import org.mockito.kotlin.verify
|
||||||
import org.mockito.kotlin.verifyNoInteractions
|
import org.mockito.kotlin.verifyNoInteractions
|
||||||
import org.mockito.kotlin.whenever
|
import org.mockito.kotlin.whenever
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
@@ -25,73 +28,72 @@ import org.springframework.test.web.servlet.post
|
|||||||
import org.springframework.web.bind.MethodArgumentNotValidException
|
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
@WebMvcTest(ProductController::class)
|
@WebMvcTest(ProductController::class)
|
||||||
class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
|
class ProductControllerTest: BaseUnitTest() {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var mockMvc: MockMvc
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
private lateinit var productService: ProductService
|
private lateinit var productService: ProductService
|
||||||
|
|
||||||
@Test
|
@MockBean
|
||||||
fun getProduct_success() {
|
private lateinit var productSyncService: ProductSyncService
|
||||||
val guid = UUID.randomUUID()
|
|
||||||
val now = OffsetDateTime.now()
|
|
||||||
val product = Product(
|
|
||||||
id = 12,
|
|
||||||
guid = guid,
|
|
||||||
name = "some",
|
|
||||||
description = null,
|
|
||||||
price = 11130,
|
|
||||||
createdAt = now,
|
|
||||||
updatedAt = null,
|
|
||||||
deletedAt = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
whenever(productService.findByGuid(
|
private val guid = UUID.randomUUID()
|
||||||
eq(guid),
|
private val now = OffsetDateTime.now()
|
||||||
)) doReturn product
|
private val productId = 12L
|
||||||
|
private val productName = "new-product"
|
||||||
|
private val productPrice = 20123L
|
||||||
|
private val product = Product(
|
||||||
|
id = productId,
|
||||||
|
guid = guid,
|
||||||
|
name = productName,
|
||||||
|
description = null,
|
||||||
|
price = productPrice,
|
||||||
|
createdAt = now,
|
||||||
|
updatedAt = null,
|
||||||
|
deletedAt = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getProduct - 200`() {
|
||||||
|
whenever(productService.findByGuid(any())).thenReturn(product)
|
||||||
|
|
||||||
mockMvc.get("/api/product/$guid")
|
mockMvc.get("/api/product/$guid")
|
||||||
.andExpect { status { isOk() } }
|
.andExpect { status { isOk() } }
|
||||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||||
.andExpect { jsonPath("\$.id") { value(product.id.toString()) } }
|
.andExpect { jsonPath("\$.id") { value(product.id.toString()) } }
|
||||||
.andExpect { jsonPath("\$.guid") { value(guid.toString()) } }
|
.andExpect { jsonPath("\$.guid") { value(guid.toString()) } }
|
||||||
.andExpect { jsonPath("\$.name") { value("some") } }
|
.andExpect { jsonPath("\$.name") { value(productName) } }
|
||||||
.andExpect { jsonPath("\$.createdAt") { value(now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) } }
|
.andExpect { jsonPath("\$.createdAt") { value(now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) } }
|
||||||
.andExpect { jsonPath("\$.updatedAt") { value(nullValue()) } }
|
.andExpect { jsonPath("\$.updatedAt") { value(nullValue()) } }
|
||||||
|
|
||||||
|
verify(productService, times(1)).findByGuid(guid)
|
||||||
|
verifyNoInteractions(productSyncService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getProduct_notFound() {
|
fun `getProduct - 404`() {
|
||||||
val guid = UUID.randomUUID()
|
whenever(productService.findByGuid(any())).thenReturn(null)
|
||||||
|
|
||||||
whenever(productService.findByGuid(
|
|
||||||
eq(guid),
|
|
||||||
)) doReturn null
|
|
||||||
|
|
||||||
mockMvc.get("/api/product/$guid")
|
mockMvc.get("/api/product/$guid")
|
||||||
.andExpect { status { isNotFound() } }
|
.andExpect { status { isNotFound() } }
|
||||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||||
.andExpect { jsonPath("\$.status") { value(ResponseStatus.NOT_FOUND.status) } }
|
.andExpect { jsonPath("\$.status") { value(ResponseStatus.NOT_FOUND.status) } }
|
||||||
|
|
||||||
|
verify(productService, times(1)).findByGuid(guid)
|
||||||
|
verifyNoInteractions(productSyncService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getProducts_success() {
|
fun `getProducts - 200`() {
|
||||||
val now = OffsetDateTime.now()
|
|
||||||
val pageRequest = PageRequest.of(1, 2, Sort.by(Sort.Direction.DESC, "createdAt"))
|
val pageRequest = PageRequest.of(1, 2, Sort.by(Sort.Direction.DESC, "createdAt"))
|
||||||
|
|
||||||
whenever(productService.findAll(
|
whenever(productService.findAll(any()))
|
||||||
pageRequest,
|
.thenReturn(PageImpl(listOf(product)))
|
||||||
)) doReturn PageImpl(listOf(Product(
|
|
||||||
id = 12,
|
|
||||||
guid = UUID.randomUUID(),
|
|
||||||
name = "some",
|
|
||||||
description = null,
|
|
||||||
price = 11130,
|
|
||||||
createdAt = now,
|
|
||||||
updatedAt = null,
|
|
||||||
deletedAt = null,
|
|
||||||
)))
|
|
||||||
|
|
||||||
mockMvc.get("/api/product?page=1&size=2&sort=createdAt,desc")
|
mockMvc.get("/api/product?page=1&size=2&sort=createdAt,desc")
|
||||||
.andExpect { status { isOk() } }
|
.andExpect { status { isOk() } }
|
||||||
@@ -99,37 +101,22 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
|
|||||||
.andExpect { jsonPath("\$.meta.total") { value(1) } }
|
.andExpect { jsonPath("\$.meta.total") { value(1) } }
|
||||||
.andExpect { jsonPath("\$.meta.pages") { value(1) } }
|
.andExpect { jsonPath("\$.meta.pages") { value(1) } }
|
||||||
.andExpect { jsonPath("\$.data") { isArray() } }
|
.andExpect { jsonPath("\$.data") { isArray() } }
|
||||||
.andExpect { jsonPath("\$.data[0].id") { value(12) } }
|
.andExpect { jsonPath("\$.data[0].id") { value(productId) } }
|
||||||
.andExpect { jsonPath("\$.data[0].name") { value("some") } }
|
.andExpect { jsonPath("\$.data[0].name") { value(productName) } }
|
||||||
.andExpect { jsonPath("\$.data[0].description") { value(null) } }
|
.andExpect { jsonPath("\$.data[0].description") { value(null) } }
|
||||||
.andExpect { jsonPath("\$.data[0].createdAt") { value(now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) } }
|
.andExpect { jsonPath("\$.data[0].createdAt") { value(now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) } }
|
||||||
.andExpect { jsonPath("\$.data[0].priceDouble") { value(111.30) } }
|
.andExpect { jsonPath("\$.data[0].priceDouble") { value(201.23) } }
|
||||||
.andExpect { jsonPath("\$.data[0].isDeleted") { value(false) } }
|
.andExpect { jsonPath("\$.data[0].isDeleted") { value(false) } }
|
||||||
|
|
||||||
|
verify(productService, times(1)).findAll(pageRequest)
|
||||||
|
verifyNoInteractions(productSyncService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun createProduct_success() {
|
fun `createProduct - 200`() {
|
||||||
val productId = 13L
|
val reqBody = """{"name":"$productName","description":null,"price":$productPrice}"""
|
||||||
val name = "new-product"
|
|
||||||
val description = null
|
|
||||||
val price = 20000L
|
|
||||||
|
|
||||||
val reqBody = """{"name":"$name","description":null,"price":$price}"""
|
whenever(productService.create(any(), any(), anyOrNull())).thenReturn(product)
|
||||||
|
|
||||||
whenever(productService.create(
|
|
||||||
eq(name),
|
|
||||||
eq(price),
|
|
||||||
eq(description)
|
|
||||||
)) doReturn Product(
|
|
||||||
id = productId,
|
|
||||||
guid = UUID.randomUUID(),
|
|
||||||
name = name,
|
|
||||||
description = description,
|
|
||||||
price = price,
|
|
||||||
createdAt = OffsetDateTime.now(),
|
|
||||||
updatedAt = null,
|
|
||||||
deletedAt = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
mockMvc.post("/api/product") {
|
mockMvc.post("/api/product") {
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
@@ -138,13 +125,14 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
|
|||||||
.andExpect { status { isCreated() } }
|
.andExpect { status { isCreated() } }
|
||||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||||
.andExpect { jsonPath("\$.id") { value(productId) } }
|
.andExpect { jsonPath("\$.id") { value(productId) } }
|
||||||
|
|
||||||
|
verify(productService, times(1)).create(productName, productPrice, null)
|
||||||
|
verifyNoInteractions(productSyncService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun createProduct_badRequest_noNameParam() {
|
fun `createProduct - 400 - no name param`() {
|
||||||
val price = 20000L
|
val reqBody = """{"description":null,"price":$productPrice}"""
|
||||||
|
|
||||||
val reqBody = """{"description":null,"price":$price}"""
|
|
||||||
|
|
||||||
mockMvc.post("/api/product") {
|
mockMvc.post("/api/product") {
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
@@ -156,13 +144,12 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
|
|||||||
.andExpect { jsonPath("\$.cause") { contains("name") } }
|
.andExpect { jsonPath("\$.cause") { contains("name") } }
|
||||||
|
|
||||||
verifyNoInteractions(productService)
|
verifyNoInteractions(productService)
|
||||||
|
verifyNoInteractions(productSyncService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun createProduct_badRequest_emptyName() {
|
fun `createProduct - 400 - empty name param`() {
|
||||||
val price = 20000L
|
val reqBody = """{"name":"","description":null,"price":$productPrice}"""
|
||||||
|
|
||||||
val reqBody = """{"name":"","description":null,"price":$price}"""
|
|
||||||
|
|
||||||
mockMvc.post("/api/product") {
|
mockMvc.post("/api/product") {
|
||||||
contentType = MediaType.APPLICATION_JSON
|
contentType = MediaType.APPLICATION_JSON
|
||||||
@@ -174,28 +161,21 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() {
|
|||||||
.andExpect { jsonPath("\$.cause") { value(MethodArgumentNotValidException::class.qualifiedName) } }
|
.andExpect { jsonPath("\$.cause") { value(MethodArgumentNotValidException::class.qualifiedName) } }
|
||||||
|
|
||||||
verifyNoInteractions(productService)
|
verifyNoInteractions(productService)
|
||||||
|
verifyNoInteractions(productSyncService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun deleteProduct_success() {
|
fun `deleteProduct - 200`() {
|
||||||
val guid = UUID.randomUUID()
|
val deletedProduct = product.copy(deletedAt = now)
|
||||||
|
|
||||||
whenever(productService.delete(
|
whenever(productService.delete(any())).thenReturn(deletedProduct)
|
||||||
eq(guid),
|
|
||||||
)) doReturn Product(
|
|
||||||
id = 2133,
|
|
||||||
guid = guid,
|
|
||||||
name = "name",
|
|
||||||
description = "description",
|
|
||||||
price = 210202,
|
|
||||||
createdAt = OffsetDateTime.now(),
|
|
||||||
updatedAt = null,
|
|
||||||
deletedAt = OffsetDateTime.now(),
|
|
||||||
)
|
|
||||||
|
|
||||||
mockMvc.delete("/api/product/${guid}")
|
mockMvc.delete("/api/product/${guid}")
|
||||||
.andExpect { status { isOk() } }
|
.andExpect { status { isOk() } }
|
||||||
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
.andExpect { content { contentType(MediaType.APPLICATION_JSON) } }
|
||||||
.andExpect { jsonPath("\$.status") { value(ResponseStatus.OK.status) } }
|
.andExpect { jsonPath("\$.status") { value(ResponseStatus.OK.status) } }
|
||||||
|
|
||||||
|
verify(productService, times(1)).delete(guid)
|
||||||
|
verifyNoInteractions(productSyncService)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.github.dannecron.demo.services
|
||||||
|
|
||||||
|
import com.github.dannecron.demo.core.dto.Product
|
||||||
|
import com.github.dannecron.demo.core.exceptions.ProductNotFoundException
|
||||||
|
import com.github.dannecron.demo.core.services.product.ProductService
|
||||||
|
import com.github.dannecron.demo.services.kafka.Producer
|
||||||
|
import com.github.dannecron.demo.services.kafka.dto.ProductDto
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
import org.mockito.kotlin.any
|
||||||
|
import org.mockito.kotlin.doReturn
|
||||||
|
import org.mockito.kotlin.mock
|
||||||
|
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
|
||||||
|
|
||||||
|
class ProductSyncServiceImplTest {
|
||||||
|
private val productService: ProductService = mock()
|
||||||
|
private val producer: Producer = mock()
|
||||||
|
|
||||||
|
private val productSyncService = ProductSyncServiceImpl(
|
||||||
|
productService = productService,
|
||||||
|
producer = producer
|
||||||
|
)
|
||||||
|
|
||||||
|
private val guid = UUID.randomUUID()
|
||||||
|
private val product = Product(
|
||||||
|
id = 123,
|
||||||
|
guid = guid,
|
||||||
|
name = "name",
|
||||||
|
description = "description",
|
||||||
|
price = 10050,
|
||||||
|
createdAt = OffsetDateTime.now().minusDays(1),
|
||||||
|
updatedAt = OffsetDateTime.now().minusHours(2),
|
||||||
|
deletedAt = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val kafkaProductDto = ProductDto(
|
||||||
|
id = 123,
|
||||||
|
guid = guid.toString(),
|
||||||
|
name = "name",
|
||||||
|
description = "description",
|
||||||
|
price = 10050,
|
||||||
|
createdAt = product.createdAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||||
|
updatedAt = product.updatedAt!!.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||||
|
deletedAt = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `syncToKafka - success`() {
|
||||||
|
whenever(productService.findByGuid(any())).thenReturn(product)
|
||||||
|
|
||||||
|
productSyncService.syncToKafka(guid, null)
|
||||||
|
|
||||||
|
verify(productService, times(1)).findByGuid(guid)
|
||||||
|
verify(producer, times(1)).produceProductSync(kafkaProductDto)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `syncToKafka - not found`() {
|
||||||
|
whenever(productService.findByGuid(any())) doReturn null
|
||||||
|
|
||||||
|
assertThrows<ProductNotFoundException> {
|
||||||
|
productSyncService.syncToKafka(guid, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(productService, times(1)).findByGuid(guid)
|
||||||
|
verifyNoInteractions(producer)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
package com.github.dannecron.demo.services.database.customer
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
|
||||||
import com.github.dannecron.demo.db.entity.City
|
|
||||||
import com.github.dannecron.demo.db.entity.Customer
|
|
||||||
import com.github.dannecron.demo.db.repository.CityRepository
|
|
||||||
import com.github.dannecron.demo.db.repository.CustomerRepository
|
|
||||||
import com.github.dannecron.demo.models.CustomerExtended
|
|
||||||
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
|
|
||||||
import org.junit.jupiter.api.assertThrows
|
|
||||||
import org.mockito.kotlin.any
|
|
||||||
import org.mockito.kotlin.doReturn
|
|
||||||
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.util.Optional
|
|
||||||
import java.util.UUID
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
class CustomerServiceImplTest {
|
|
||||||
private val mockGuid = UUID.randomUUID()
|
|
||||||
private val mockCurrentTime = OffsetDateTime.now()
|
|
||||||
|
|
||||||
private val commonGenerator: CommonGenerator = mock {
|
|
||||||
on { generateUUID() } doReturn mockGuid
|
|
||||||
on { generateCurrentTime() } doReturn mockCurrentTime
|
|
||||||
}
|
|
||||||
|
|
||||||
private val customerRepository: CustomerRepository = mock()
|
|
||||||
private val cityRepository: CityRepository = mock()
|
|
||||||
private val customerServiceImpl = CustomerServiceImpl(
|
|
||||||
customerRepository = customerRepository,
|
|
||||||
cityRepository = cityRepository,
|
|
||||||
commonGenerator = commonGenerator,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val cityId = 123L
|
|
||||||
private val cityGuid = UUID.randomUUID()
|
|
||||||
private val customer = Customer(
|
|
||||||
id = 1,
|
|
||||||
guid = mockGuid,
|
|
||||||
name = "name",
|
|
||||||
cityId = cityId,
|
|
||||||
createdAt = mockCurrentTime,
|
|
||||||
updatedAt = null,
|
|
||||||
)
|
|
||||||
private val city = City(
|
|
||||||
id = cityId,
|
|
||||||
guid = cityGuid,
|
|
||||||
name = "city",
|
|
||||||
createdAt = OffsetDateTime.now(),
|
|
||||||
updatedAt = null,
|
|
||||||
deletedAt = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `create - success - with city`() {
|
|
||||||
whenever(customerRepository.save(any<Customer>())).thenReturn(customer)
|
|
||||||
whenever(cityRepository.findByGuid(cityGuid)).thenReturn(city)
|
|
||||||
|
|
||||||
val result = customerServiceImpl.create("name", cityGuid)
|
|
||||||
assertEquals(customer, result)
|
|
||||||
|
|
||||||
verify(customerRepository, times(1)).save(customer.copy(id = null))
|
|
||||||
verify(cityRepository, times(1)).findByGuid(cityGuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `create - success - no city`() {
|
|
||||||
val customerNoCity = customer.copy(cityId = null)
|
|
||||||
|
|
||||||
whenever(customerRepository.save(any<Customer>())).thenReturn(customerNoCity)
|
|
||||||
|
|
||||||
val result = customerServiceImpl.create("name", null)
|
|
||||||
assertEquals(customerNoCity, result)
|
|
||||||
|
|
||||||
verify(customerRepository, times(1)).save(customerNoCity.copy(id = null))
|
|
||||||
verifyNoInteractions(cityRepository)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `create - fail - with city`() {
|
|
||||||
whenever(customerRepository.save(any<Customer>())).thenReturn(customer)
|
|
||||||
whenever(cityRepository.findByGuid(cityGuid)).thenReturn(null)
|
|
||||||
|
|
||||||
assertThrows<CityNotFoundException> {
|
|
||||||
customerServiceImpl.create("name", cityGuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
verify(customerRepository, never()).save(customer.copy(id = null))
|
|
||||||
verify(cityRepository, times(1)).findByGuid(cityGuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `findByGuid - with city`() {
|
|
||||||
val customerGuid = mockGuid
|
|
||||||
whenever(customerRepository.findByGuid(any())).thenReturn(customer)
|
|
||||||
whenever(cityRepository.findById(any())).thenReturn(Optional.of(city))
|
|
||||||
|
|
||||||
val result = customerServiceImpl.findByGuid(customerGuid)
|
|
||||||
assertEquals(CustomerExtended(customer, city), result)
|
|
||||||
|
|
||||||
verify(customerRepository, times(1)).findByGuid(customerGuid)
|
|
||||||
verify(cityRepository, times(1)).findById(cityId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `findByGuid - no city`() {
|
|
||||||
val customerGuid = mockGuid
|
|
||||||
whenever(customerRepository.findByGuid(any())).thenReturn(customer)
|
|
||||||
whenever(cityRepository.findById(any())).thenReturn(Optional.empty())
|
|
||||||
|
|
||||||
val result = customerServiceImpl.findByGuid(customerGuid)
|
|
||||||
assertEquals(CustomerExtended(customer, null), result)
|
|
||||||
|
|
||||||
verify(customerRepository, times(1)).findByGuid(customerGuid)
|
|
||||||
verify(cityRepository, times(1)).findById(cityId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package com.github.dannecron.demo.services.database.order
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
|
||||||
import com.github.dannecron.demo.db.entity.Customer
|
|
||||||
import com.github.dannecron.demo.db.entity.Product
|
|
||||||
import com.github.dannecron.demo.db.entity.order.Order
|
|
||||||
import com.github.dannecron.demo.db.entity.order.OrderProduct
|
|
||||||
import com.github.dannecron.demo.db.repository.OrderProductRepository
|
|
||||||
import com.github.dannecron.demo.db.repository.OrderRepository
|
|
||||||
import com.github.dannecron.demo.db.repository.ProductRepository
|
|
||||||
import com.github.dannecron.demo.models.OrderWithProducts
|
|
||||||
import org.mockito.kotlin.any
|
|
||||||
import org.mockito.kotlin.doReturn
|
|
||||||
import org.mockito.kotlin.mock
|
|
||||||
import org.mockito.kotlin.times
|
|
||||||
import org.mockito.kotlin.verify
|
|
||||||
import org.mockito.kotlin.whenever
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.util.UUID
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
class OrderServiceImplTest {
|
|
||||||
private val mockGuid = UUID.randomUUID()
|
|
||||||
private val mockCurrentTime = OffsetDateTime.now()
|
|
||||||
|
|
||||||
private val commonGenerator: CommonGenerator = mock {
|
|
||||||
on { generateUUID() } doReturn mockGuid
|
|
||||||
on { generateCurrentTime() } doReturn mockCurrentTime
|
|
||||||
}
|
|
||||||
|
|
||||||
private val orderRepository: OrderRepository = mock()
|
|
||||||
private val productRepository: ProductRepository = mock()
|
|
||||||
private val orderProductRepository: OrderProductRepository = mock()
|
|
||||||
|
|
||||||
private val orderServiceImpl = OrderServiceImpl(
|
|
||||||
orderRepository = orderRepository,
|
|
||||||
orderProductRepository = orderProductRepository,
|
|
||||||
productRepository = productRepository,
|
|
||||||
commonGenerator = commonGenerator,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val now = OffsetDateTime.now()
|
|
||||||
|
|
||||||
private val customerId = 123L
|
|
||||||
private val customer = Customer(
|
|
||||||
id = customerId,
|
|
||||||
guid = UUID.randomUUID(),
|
|
||||||
name = "customer",
|
|
||||||
cityId = null,
|
|
||||||
createdAt = now,
|
|
||||||
updatedAt = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val orderOneId = 1001L
|
|
||||||
private val orderTwoId = 1002L
|
|
||||||
private val orderOne = Order(
|
|
||||||
id = orderOneId,
|
|
||||||
guid = UUID.randomUUID(),
|
|
||||||
customerId = customerId,
|
|
||||||
deliveredAt = now.minusHours(1),
|
|
||||||
createdAt = now.minusDays(1),
|
|
||||||
updatedAt = now.minusHours(1),
|
|
||||||
)
|
|
||||||
private val orderTwo = Order(
|
|
||||||
id = orderTwoId,
|
|
||||||
guid = UUID.randomUUID(),
|
|
||||||
customerId = customerId,
|
|
||||||
deliveredAt = null,
|
|
||||||
createdAt = now,
|
|
||||||
updatedAt = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val productId = 100L
|
|
||||||
private val product = Product(
|
|
||||||
id = productId,
|
|
||||||
guid = UUID.randomUUID(),
|
|
||||||
name = "product",
|
|
||||||
description = null,
|
|
||||||
price = 10000L,
|
|
||||||
createdAt = now.minusMonths(1),
|
|
||||||
updatedAt = null,
|
|
||||||
deletedAt = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val orderProduct = OrderProduct(
|
|
||||||
guid = UUID.randomUUID(),
|
|
||||||
orderId = orderOneId,
|
|
||||||
productId = productId,
|
|
||||||
createdAt = now.minusDays(1),
|
|
||||||
updatedAt = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun findByCustomerId() {
|
|
||||||
whenever(orderRepository.findByCustomerId(any())).thenReturn(listOf(orderOne, orderTwo))
|
|
||||||
whenever(orderProductRepository.findByOrderId(any()))
|
|
||||||
.thenReturn(listOf(orderProduct))
|
|
||||||
.thenReturn(emptyList())
|
|
||||||
whenever(productRepository.findAllById(any())).thenReturn(listOf(product))
|
|
||||||
|
|
||||||
val expectedResult = listOf(
|
|
||||||
OrderWithProducts(
|
|
||||||
order = orderOne,
|
|
||||||
products = listOf(product),
|
|
||||||
),
|
|
||||||
OrderWithProducts(
|
|
||||||
order = orderTwo,
|
|
||||||
products = emptyList(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
val result = orderServiceImpl.findByCustomerId(customerId)
|
|
||||||
assertEquals(expectedResult, result)
|
|
||||||
|
|
||||||
verify(orderRepository, times(1)).findByCustomerId(customerId)
|
|
||||||
verify(orderProductRepository, times(1)).findByOrderId(orderOneId)
|
|
||||||
verify(orderProductRepository, times(1)).findByOrderId(orderTwoId)
|
|
||||||
verify(productRepository, times(1)).findAllById(listOf(productId))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun create() {
|
|
||||||
val newOrder = orderTwo.copy(
|
|
||||||
guid = mockGuid,
|
|
||||||
createdAt = mockCurrentTime,
|
|
||||||
)
|
|
||||||
val newOrderProduct = orderProduct.copy(
|
|
||||||
guid = mockGuid,
|
|
||||||
createdAt = mockCurrentTime,
|
|
||||||
orderId = orderTwoId,
|
|
||||||
)
|
|
||||||
|
|
||||||
whenever(orderRepository.save(any<Order>())).thenReturn(newOrder)
|
|
||||||
whenever(orderProductRepository.saveAll(any<List<OrderProduct>>())).thenReturn(listOf(newOrderProduct))
|
|
||||||
|
|
||||||
val result = orderServiceImpl.createOrder(
|
|
||||||
customer = customer,
|
|
||||||
products = setOf(product),
|
|
||||||
)
|
|
||||||
|
|
||||||
assertEquals(newOrder, result)
|
|
||||||
|
|
||||||
verify(orderRepository, times(1)).save(newOrder.copy(id = null))
|
|
||||||
verify(orderProductRepository, times(1)).saveAll(listOf(newOrderProduct))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package com.github.dannecron.demo.services.kafka
|
|
||||||
|
|
||||||
import com.github.dannecron.demo.models.City
|
|
||||||
import com.github.dannecron.demo.services.database.city.CityService
|
|
||||||
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
|
||||||
import io.micrometer.core.instrument.MeterRegistry
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import org.mockito.kotlin.after
|
|
||||||
import org.mockito.kotlin.any
|
|
||||||
import org.mockito.kotlin.doReturn
|
|
||||||
import org.mockito.kotlin.verify
|
|
||||||
import org.mockito.kotlin.whenever
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean
|
|
||||||
import org.springframework.kafka.core.KafkaTemplate
|
|
||||||
import org.springframework.kafka.support.KafkaHeaders
|
|
||||||
import org.springframework.kafka.test.context.EmbeddedKafka
|
|
||||||
import org.springframework.messaging.Message
|
|
||||||
import org.springframework.messaging.support.MessageBuilder
|
|
||||||
import org.springframework.test.annotation.DirtiesContext
|
|
||||||
import org.springframework.test.context.ActiveProfiles
|
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.util.UUID
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
@ActiveProfiles("kafka")
|
|
||||||
@SpringBootTest
|
|
||||||
@EmbeddedKafka(
|
|
||||||
brokerProperties = ["listeners=PLAINTEXT://localhost:3392", "port=3392"],
|
|
||||||
topics = ["demo-city-sync"],
|
|
||||||
partitions = 1,
|
|
||||||
)
|
|
||||||
@DirtiesContext
|
|
||||||
class ConsumerKfkTest {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private lateinit var kafkaTemplate: KafkaTemplate<String, Any>
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private lateinit var metricRegistry: MeterRegistry
|
|
||||||
|
|
||||||
@MockBean
|
|
||||||
private lateinit var cityService: CityService
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun consumer_handleCityCreate() {
|
|
||||||
val cityGuid = UUID.randomUUID()
|
|
||||||
val cityName = "new-city"
|
|
||||||
val createdAt = OffsetDateTime.now().minusDays(1)
|
|
||||||
val cityCreateDto = CityCreateDto(
|
|
||||||
guid = cityGuid.toString(),
|
|
||||||
name = cityName,
|
|
||||||
createdAt = createdAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
|
||||||
updatedAt = null,
|
|
||||||
deletedAt = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
whenever(cityService.create(any<CityCreateDto>())) doReturn City(
|
|
||||||
id = 123,
|
|
||||||
guid = cityGuid,
|
|
||||||
name = cityName,
|
|
||||||
createdAt = createdAt,
|
|
||||||
updatedAt = null,
|
|
||||||
deletedAt = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
val message: Message<String> = MessageBuilder
|
|
||||||
.withPayload(
|
|
||||||
Json.encodeToString(cityCreateDto)
|
|
||||||
)
|
|
||||||
.setHeader(KafkaHeaders.TOPIC, "demo-city-sync")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
kafkaTemplate.send(message)
|
|
||||||
|
|
||||||
verify(cityService, after(1000).times(1)).create(cityCreateDto)
|
|
||||||
|
|
||||||
assertEquals(1.0, metricRegistry.get("kafka_consumer_city_create").counter().count())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +1,62 @@
|
|||||||
package com.github.dannecron.demo.services.kafka
|
package com.github.dannecron.demo.services.kafka
|
||||||
|
|
||||||
import com.github.dannecron.demo.BaseUnitTest
|
import com.github.dannecron.demo.core.services.validation.SchemaValidator
|
||||||
import com.github.dannecron.demo.models.Product
|
|
||||||
import com.github.dannecron.demo.services.kafka.dto.ProductDto
|
import com.github.dannecron.demo.services.kafka.dto.ProductDto
|
||||||
import com.github.dannecron.demo.services.validation.SchemaValidator
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.encodeToJsonElement
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
import org.junit.runner.RunWith
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.mockito.kotlin.*
|
import org.mockito.kotlin.any
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.mockito.kotlin.argumentCaptor
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.mockito.kotlin.eq
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean
|
import org.mockito.kotlin.mock
|
||||||
import org.springframework.kafka.core.KafkaTemplate
|
import org.mockito.kotlin.times
|
||||||
import org.springframework.kafka.support.KafkaHeaders
|
import org.mockito.kotlin.verify
|
||||||
import org.springframework.kafka.support.SendResult
|
import org.mockito.kotlin.whenever
|
||||||
|
import org.springframework.cloud.stream.function.StreamBridge
|
||||||
import org.springframework.messaging.Message
|
import org.springframework.messaging.Message
|
||||||
import org.springframework.test.context.junit4.SpringRunner
|
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.*
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.UUID
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
@RunWith(SpringRunner::class)
|
class ProducerImplTest {
|
||||||
@SpringBootTest
|
private val streamBridge: StreamBridge = mock()
|
||||||
class ProducerImplTest: BaseUnitTest() {
|
private val schemaValidator: SchemaValidator = mock()
|
||||||
@Autowired
|
private val producerImpl = ProducerImpl(
|
||||||
private lateinit var producerImpl: ProducerImpl
|
streamBridge = streamBridge,
|
||||||
|
schemaValidator = schemaValidator,
|
||||||
@MockBean
|
)
|
||||||
private lateinit var kafkaTemplate: KafkaTemplate<String, Any>
|
|
||||||
|
|
||||||
@MockBean
|
|
||||||
private lateinit var schemaValidator: SchemaValidator
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun produceProductInfo_success() {
|
fun produceProductSync_success() {
|
||||||
val topic = "some-topic"
|
val guid = UUID.randomUUID()
|
||||||
val product = Product(
|
val createdAt = OffsetDateTime.now().minusDays(2)
|
||||||
|
val updatedAt = OffsetDateTime.now().minusHours(1)
|
||||||
|
val productDto = ProductDto(
|
||||||
id = 123,
|
id = 123,
|
||||||
guid = UUID.randomUUID(),
|
guid = guid.toString(),
|
||||||
name = "name",
|
name = "name",
|
||||||
description = null,
|
description = null,
|
||||||
price = 10050,
|
price = 10050,
|
||||||
createdAt = OffsetDateTime.now().minusDays(2),
|
createdAt = createdAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||||
updatedAt = OffsetDateTime.now().minusHours(1),
|
updatedAt = updatedAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||||
deletedAt = OffsetDateTime.now(),
|
deletedAt = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
val captor = argumentCaptor<Message<String>>()
|
val captor = argumentCaptor<Message<String>>()
|
||||||
|
|
||||||
whenever(kafkaTemplate.send(captor.capture())) doReturn CompletableFuture<SendResult<String, Any>>()
|
whenever(streamBridge.send(any(), captor.capture())).thenReturn(true)
|
||||||
|
|
||||||
whenever(schemaValidator.validate(
|
producerImpl.produceProductSync(productDto)
|
||||||
eq("product-sync"),
|
|
||||||
eq(Json.encodeToJsonElement(product))
|
|
||||||
)) doAnswer { }
|
|
||||||
|
|
||||||
producerImpl.produceProductInfo(topic, product)
|
|
||||||
|
|
||||||
assertEquals(1, captor.allValues.count())
|
assertEquals(1, captor.allValues.count())
|
||||||
val actualArgument = captor.firstValue
|
val actualArgument = captor.firstValue
|
||||||
|
|
||||||
val actualProductDto = Json.decodeFromString<ProductDto>(actualArgument.payload)
|
val actualProductDto = Json.decodeFromString<ProductDto>(actualArgument.payload)
|
||||||
assertEquals(product.id, actualProductDto.id)
|
assertEquals(productDto, actualProductDto)
|
||||||
assertEquals(product.guid.toString(), actualProductDto.guid)
|
|
||||||
assertEquals(topic, actualArgument.headers[KafkaHeaders.TOPIC])
|
|
||||||
assertEquals("some-custom-header", actualArgument.headers["X-Custom-Header"])
|
assertEquals("some-custom-header", actualArgument.headers["X-Custom-Header"])
|
||||||
|
|
||||||
|
verify(streamBridge, times(1)).send(eq("productSyncProducer"), any())
|
||||||
|
verify(schemaValidator, times(1)).validate("kafka-product-sync", Json.encodeToJsonElement(productDto))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user