move validation to separate core package, add generation service

some code cleanup
This commit is contained in:
Savosin Denis
2025-05-05 12:54:17 +07:00
parent d7c051746d
commit 7c12208883
42 changed files with 669 additions and 713 deletions

View File

@@ -69,13 +69,13 @@ subprojects {
dependencies {
implementation(project(":db"))
implementation(project(":core"))
runtimeOnly(libs.micrometer.registry.prometheus)
implementation(libs.bundles.tracing)
implementation(libs.jackson.datatype.jsr)
implementation(libs.jackson.module.kotlin)
implementation(libs.json.schema.validator)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.core)
implementation(libs.logback.encoder)
@@ -85,13 +85,14 @@ dependencies {
implementation(libs.spring.boot.starter.mustache)
implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.web)
implementation(libs.spring.cloud.starter.streamKafka)
implementation(libs.spring.doc.openapi.starter)
implementation(libs.spring.kafka)
testImplementation(libs.spring.kafka.test)
testImplementation(libs.ktor.client.mock)
testImplementation(libs.spring.boot.starter.actuatorAutoconfigure)
testImplementation(libs.spring.cloud.starter.streamTestBinder)
testImplementation(libs.testcontainers)
testImplementation(libs.testcontainers.junit.jupiter)
testImplementation(libs.ktor.client.mock)
developmentOnly(libs.spring.boot.devtools)
}

7
core/build.gradle.kts Normal file
View File

@@ -0,0 +1,7 @@
group = "com.github.dannecron.demo"
version = "single-version"
dependencies {
implementation(rootProject.libs.spring.boot.starter.validation)
implementation(rootProject.libs.json.schema.validator)
}

View File

@@ -0,0 +1,22 @@
package com.github.dannecron.demo.core.config
import com.github.dannecron.demo.core.config.properties.ValidationProperties
import com.github.dannecron.demo.core.services.validation.SchemaValidator
import com.github.dannecron.demo.core.services.validation.SchemaValidatorImp
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.util.ResourceUtils
@Configuration
class SchemaValidationConfig(
private val validationProperties: ValidationProperties,
) {
@Bean
fun schemaValidator(): SchemaValidator = SchemaValidatorImp(
schemaMap = validationProperties.schema.mapValues {
schema -> ResourceUtils.getFile("classpath:json-schemas/${schema.value}")
.readText(Charsets.UTF_8)
}
)
}

View File

@@ -1,4 +1,4 @@
package com.github.dannecron.demo.config.properties
package com.github.dannecron.demo.core.config.properties
import org.springframework.boot.context.properties.ConfigurationProperties

View File

@@ -0,0 +1,11 @@
package com.github.dannecron.demo.core.services.generation
import java.time.OffsetDateTime
import java.util.UUID
interface CommonGenerator {
fun generateUUID(): UUID
fun generateCurrentTime(): OffsetDateTime
}

View File

@@ -0,0 +1,16 @@
package com.github.dannecron.demo.core.services.generation
import org.springframework.stereotype.Component
import java.time.OffsetDateTime
import java.util.UUID
@Component
class CommonGeneratorImpl : CommonGenerator {
override fun generateUUID(): UUID {
return UUID.randomUUID()
}
override fun generateCurrentTime(): OffsetDateTime {
return OffsetDateTime.now()
}
}

View File

@@ -0,0 +1,11 @@
package com.github.dannecron.demo.core.services.validation
import com.github.dannecron.demo.core.services.validation.exceptions.ElementNotValidException
import com.github.dannecron.demo.core.services.validation.exceptions.SchemaNotFoundException
import kotlinx.serialization.json.JsonElement
interface SchemaValidator {
@Throws(ElementNotValidException::class, SchemaNotFoundException::class)
fun validate(schemaName: String, value: JsonElement)
}

View File

@@ -0,0 +1,32 @@
package com.github.dannecron.demo.core.services.validation
import com.github.dannecron.demo.core.services.validation.exceptions.ElementNotValidException
import com.github.dannecron.demo.core.services.validation.exceptions.SchemaNotFoundException
import io.github.optimumcode.json.schema.JsonSchema
import io.github.optimumcode.json.schema.ValidationError
import kotlinx.serialization.json.JsonElement
class SchemaValidatorImp(
private val schemaMap: Map<String, String>,
): SchemaValidator {
@Throws(
SchemaNotFoundException::class,
ElementNotValidException::class,
)
override fun validate(schemaName: String, value: JsonElement) {
JsonSchema.fromDefinition(
getSchema(schemaName),
).also {
val errors = mutableListOf<ValidationError>()
if (!it.validate(value, errors::add)) {
throw ElementNotValidException(errors)
}
}
}
@Throws(SchemaNotFoundException::class)
private fun getSchema(schemaName: String) = schemaMap[schemaName]
?: throw SchemaNotFoundException()
}

View File

@@ -1,4 +1,4 @@
package com.github.dannecron.demo.services.validation.exceptions
package com.github.dannecron.demo.core.services.validation.exceptions
import io.github.optimumcode.json.schema.ValidationError

View File

@@ -0,0 +1,3 @@
package com.github.dannecron.demo.core.services.validation.exceptions
class SchemaNotFoundException: RuntimeException()

View File

@@ -1,25 +1,20 @@
package com.github.dannecron.demo.services.validation
package com.github.dannecron.demo.core.services.validation
import com.github.dannecron.demo.BaseUnitTest
import com.github.dannecron.demo.services.validation.SchemaValidator.Companion.SCHEMA_KAFKA_PRODUCT_SYNC
import com.github.dannecron.demo.services.validation.exceptions.ElementNotValidException
import com.github.dannecron.demo.services.validation.exceptions.SchemaNotFoundException
import com.github.dannecron.demo.core.services.validation.exceptions.ElementNotValidException
import com.github.dannecron.demo.core.services.validation.exceptions.SchemaNotFoundException
import kotlinx.serialization.json.Json
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner
import kotlin.reflect.KClass
import kotlin.test.assertFailsWith
@RunWith(SpringRunner::class)
@SpringBootTest
class SchemaValidatorImpTest(
@Autowired val schemaValidatorImp: SchemaValidatorImp
): BaseUnitTest() {
class SchemaValidatorImpTest {
private val schemaValidatorImp = SchemaValidatorImp(
schemaMap = mapOf(
KAFKA_PRODUCT_SYNC_SCHEMA to getJsonSchema("json-schemas/kafka/product/sync.json")),
)
@ParameterizedTest
@MethodSource("validateDataProvider")
fun validate(schemaName: String, inputRawJson: String, expectedException: KClass<out Throwable>?) {
@@ -27,8 +22,6 @@ class SchemaValidatorImpTest(
if (expectedException == null) {
schemaValidatorImp.validate(schemaName = schemaName, value = element)
// second time should use cache
schemaValidatorImp.validate(schemaName = schemaName, value = element)
return
}
@@ -39,10 +32,12 @@ class SchemaValidatorImpTest(
}
companion object {
private const val KAFKA_PRODUCT_SYNC_SCHEMA = "kafkaProductSync"
@JvmStatic
fun validateDataProvider() = listOf(
Arguments.of(
SCHEMA_KAFKA_PRODUCT_SYNC,
KAFKA_PRODUCT_SYNC_SCHEMA,
"""
{
"id": 123,
@@ -58,7 +53,7 @@ class SchemaValidatorImpTest(
null,
),
Arguments.of( // no id
SCHEMA_KAFKA_PRODUCT_SYNC,
KAFKA_PRODUCT_SYNC_SCHEMA,
"""
{
"guid": "3a27e322-b5b6-427f-b761-a02284c1cfa4",
@@ -73,7 +68,7 @@ class SchemaValidatorImpTest(
ElementNotValidException::class,
),
Arguments.of( // wrong guid
SCHEMA_KAFKA_PRODUCT_SYNC,
KAFKA_PRODUCT_SYNC_SCHEMA,
"""
{
"id": 213,
@@ -95,4 +90,9 @@ class SchemaValidatorImpTest(
)
)
}
private fun getJsonSchema(resourcePath: String) = javaClass.classLoader
.getResourceAsStream(resourcePath)!!
.readAllBytes()
.toString(Charsets.UTF_8)
}

View File

@@ -3,7 +3,7 @@ jackson = "2.15.4"
kotlin = "2.1.10"
ktor = "3.0.0"
spring-boot = "3.2.10"
spring-kafka = "3.1.3"
spring-cloud = "3.2.10"
testcontainers = "1.19.7"
[libraries]
@@ -26,14 +26,15 @@ postgres = { module = "org.postgresql:postgresql", version = "42.6.2" }
spring-aspects = { module = "org.springframework:spring-aspects" }
spring-boot-devtools = { module = "org.springframework.boot:spring-boot-devtools" }
spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "spring-boot" }
spring-boot-starter-actuatorAutoconfigure = { module = "org.springframework.boot:spring-boot-actuator-autoconfigure" }
spring-boot-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-data-jdbc", version.ref = "spring-boot"}
spring-boot-starter-mustache = { module = "org.springframework.boot:spring-boot-starter-mustache", version.ref = "spring-boot" }
spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "spring-boot" }
spring-boot-starter-validation = { module = "org.springframework.boot:spring-boot-starter-validation", version.ref = "spring-boot" }
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
spring-cloud-starter-streamKafka = { module = "org.springframework.cloud:spring-cloud-starter-stream-kafka", version.ref = "spring-cloud"}
spring-cloud-starter-streamTestBinder = { module = "org.springframework.cloud:spring-cloud-stream-test-binder", version = "4.0.4"}
spring-doc-openapi-starter = "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0"
spring-kafka = { module = "org.springframework.kafka:spring-kafka", version.ref = "spring-kafka"}
spring-kafka-test = { module = "org.springframework.kafka:spring-kafka-test", version.ref = "spring-kafka"}
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers"}
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers"}
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers"}

View File

@@ -1,2 +1,6 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "demo"
include("db")
include("core")

View File

@@ -1,31 +0,0 @@
package com.github.dannecron.demo.models
import com.github.dannecron.demo.models.serializables.OffsetDateTimeSerialization
import com.github.dannecron.demo.models.serializables.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.*
@Table("city")
@Serializable
data class City(
@Id
val id: Long?,
@Serializable(with = UuidSerialization::class)
val guid: UUID,
val name: String,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "deleted_at")
val deletedAt: OffsetDateTime?,
) {
fun isDeleted(): Boolean = deletedAt != null
}

View File

@@ -1,27 +0,0 @@
package com.github.dannecron.demo.models
import com.github.dannecron.demo.models.serializables.OffsetDateTimeSerialization
import com.github.dannecron.demo.models.serializables.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.*
@Table("customer")
@Serializable
data class Customer(
@Id
val id: Long?,
@Serializable(with = UuidSerialization::class)
val guid: UUID,
val name: String,
val cityId: Long?,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?,
)

View File

@@ -1,5 +1,7 @@
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

View File

@@ -1,6 +1,7 @@
package com.github.dannecron.demo.models.order
package com.github.dannecron.demo.models
import com.github.dannecron.demo.models.Product
import com.github.dannecron.demo.db.entity.Product
import com.github.dannecron.demo.db.entity.order.Order
data class OrderWithProducts(
val order: Order,

View File

@@ -1,37 +0,0 @@
package com.github.dannecron.demo.models
import com.github.dannecron.demo.models.serializables.OffsetDateTimeSerialization
import com.github.dannecron.demo.models.serializables.UuidSerialization
import com.github.dannecron.demo.utils.roundTo
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.*
@Table(value = "product")
@Serializable
data class Product(
@Id
val id: Long?,
@Serializable(with = UuidSerialization::class)
val guid: UUID,
val name: String,
val description: String?,
val price: Long,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "deleted_at")
val deletedAt: OffsetDateTime?,
) {
fun getPriceDouble(): Double = (price.toDouble() / 100).roundTo(2)
fun isDeleted(): Boolean = deletedAt != null
}

View File

@@ -1,31 +0,0 @@
package com.github.dannecron.demo.models.order
import com.github.dannecron.demo.models.serializables.OffsetDateTimeSerialization
import com.github.dannecron.demo.models.serializables.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.*
@Table(value = "order")
@Serializable
data class Order(
@Id
val id: Long?,
@Serializable(with = UuidSerialization::class)
val guid: UUID,
val customerId: Long,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "delivered_at")
val deliveredAt: OffsetDateTime?,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?
) {
fun isDelivered(): Boolean = deliveredAt != null
}

View File

@@ -1,41 +0,0 @@
package com.github.dannecron.demo.models.order
import com.github.dannecron.demo.models.serializables.OffsetDateTimeSerialization
import com.github.dannecron.demo.models.serializables.UuidSerialization
import kotlinx.serialization.Serializable
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.Transient
import org.springframework.data.domain.Persistable
import org.springframework.data.relational.core.mapping.Column
import org.springframework.data.relational.core.mapping.Table
import java.time.OffsetDateTime
import java.util.*
@Table(value = "order_product")
@Serializable
data class OrderProduct(
@Id
@Serializable(with = UuidSerialization::class)
val guid: UUID,
@Column(value = "order_id")
val orderId: Long,
@Column(value = "product_id")
val productId: Long,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "created_at")
val createdAt: OffsetDateTime,
@Serializable(with = OffsetDateTimeSerialization::class)
@Column(value = "updated_at")
val updatedAt: OffsetDateTime?,
): Persistable<UUID> {
@Transient
var isNewInstance: Boolean? = null
override fun getId(): UUID {
return guid
}
override fun isNew(): Boolean {
return isNewInstance ?: true
}
}

View File

@@ -1,19 +0,0 @@
package com.github.dannecron.demo.models.serializables
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
object OffsetDateTimeSerialization: KSerializer<OffsetDateTime> {
override val descriptor = PrimitiveSerialDescriptor("Time", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): OffsetDateTime = OffsetDateTime.parse(decoder.decodeString())
override fun serialize(encoder: Encoder, value: OffsetDateTime) {
encoder.encodeString(value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
}
}

View File

@@ -1,18 +0,0 @@
package com.github.dannecron.demo.models.serializables
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.*
object UuidSerialization: KSerializer<UUID> {
override val descriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
override fun serialize(encoder: Encoder, value: UUID) {
encoder.encodeString(value.toString())
}
}

View File

@@ -1,6 +1,6 @@
package com.github.dannecron.demo.services.database.city
import com.github.dannecron.demo.models.City
import com.github.dannecron.demo.db.entity.City
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto

View File

@@ -1,24 +1,28 @@
package com.github.dannecron.demo.services.database.city
import com.github.dannecron.demo.models.City
import com.github.dannecron.demo.providers.CityRepository
import com.github.dannecron.demo.core.services.generation.CommonGenerator
import com.github.dannecron.demo.db.entity.City
import com.github.dannecron.demo.db.repository.CityRepository
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
import org.springframework.stereotype.Service
import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.UUID
@Service
class CityServiceImpl(
private val cityRepository: CityRepository
private val cityRepository: CityRepository,
private val commonGenerator: CommonGenerator,
): CityService {
override fun findByGuid(guid: UUID): City? = cityRepository.findByGuid(guid)
override fun create(name: String): City = City(
id = null,
guid = UUID.randomUUID(),
guid = commonGenerator.generateUUID(),
name = name,
createdAt = OffsetDateTime.now(),
createdAt = commonGenerator.generateCurrentTime(),
updatedAt = null,
deletedAt = null,
).let {

View File

@@ -1,6 +1,6 @@
package com.github.dannecron.demo.services.database.customer
import com.github.dannecron.demo.models.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.*

View File

@@ -1,16 +1,19 @@
package com.github.dannecron.demo.services.database.customer
import com.github.dannecron.demo.models.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.providers.CityRepository
import com.github.dannecron.demo.providers.CustomerRepository
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
import java.time.OffsetDateTime
import java.util.*
import org.springframework.stereotype.Service
import java.util.UUID
@Service
class CustomerServiceImpl(
private val customerRepository: CustomerRepository,
private val cityRepository: CityRepository
private val cityRepository: CityRepository,
private val commonGenerator: CommonGenerator,
): CustomerService {
override fun findByGuid(guid: UUID): CustomerExtended? = customerRepository.findByGuid(guid)
?.let {
@@ -22,12 +25,12 @@ class CustomerServiceImpl(
override fun create(name: String, cityGuid: UUID?): Customer = Customer(
id = null,
guid = UUID.randomUUID(),
guid = commonGenerator.generateUUID(),
name = name,
cityId = cityGuid?.let {
cityRepository.findByGuid(it)?.id ?: throw CityNotFoundException()
},
createdAt = OffsetDateTime.now(),
createdAt = commonGenerator.generateCurrentTime(),
updatedAt = null,
).let {
customerRepository.save(it)

View File

@@ -1,55 +1,65 @@
package com.github.dannecron.demo.services.database.order
import com.github.dannecron.demo.models.Customer
import com.github.dannecron.demo.models.Product
import com.github.dannecron.demo.models.order.Order
import com.github.dannecron.demo.models.order.OrderProduct
import com.github.dannecron.demo.models.order.OrderWithProducts
import com.github.dannecron.demo.providers.OrderProductRepository
import com.github.dannecron.demo.providers.OrderRepository
import com.github.dannecron.demo.providers.ProductRepository
import org.springframework.beans.factory.annotation.Autowired
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
import java.time.OffsetDateTime
import java.util.*
@Service
class OrderServiceImpl(
@Autowired private val orderRepository: OrderRepository,
@Autowired private val orderProductRepository: OrderProductRepository,
@Autowired private val productRepository: ProductRepository,
private val orderRepository: OrderRepository,
private val orderProductRepository: OrderProductRepository,
private val productRepository: ProductRepository,
private val commonGenerator: CommonGenerator,
) {
fun getByCustomerId(customerId: Long): List<OrderWithProducts> = orderRepository.findByCustomerId(customerId)
fun findByCustomerId(customerId: Long): List<OrderWithProducts> = orderRepository.findByCustomerId(customerId)
.let { orders -> orders.map { order -> OrderWithProducts(
order = order,
products = orderProductRepository.findByOrderId(orderId = order.id!!)
.map { orderProduct -> orderProduct.productId }
.let { productIds -> productRepository.findAllById(productIds).toList() }
products = findProductsByOrderId(order.id!!),
) } }
@Transactional
fun createOrder(customer: Customer, products: Set<Product>): Order {
val order = Order(
id = null,
guid = UUID.randomUUID(),
guid = commonGenerator.generateUUID(),
customerId = customer.id!!,
deliveredAt = null,
createdAt = OffsetDateTime.now(),
createdAt = commonGenerator.generateCurrentTime(),
updatedAt = null,
)
return orderRepository.save(order)
.also {
savedOrder -> products.toList()
.map { product -> OrderProduct(
guid = UUID.randomUUID(),
orderId = savedOrder.id!!,
productId = product.id!!,
createdAt = OffsetDateTime.now(),
updatedAt = null
) }
.also { orderProductRepository.saveAll(it) }
.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) }
}
}

View File

@@ -1,13 +1,13 @@
package com.github.dannecron.demo.services.database.product
import com.github.dannecron.demo.models.Product
import com.github.dannecron.demo.db.entity.Product
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import java.util.*
import java.util.UUID
@Service
interface ProductService {

View File

@@ -1,21 +1,26 @@
package com.github.dannecron.demo.services.database.product
import com.github.dannecron.demo.models.Product
import com.github.dannecron.demo.providers.ProductRepository
import com.github.dannecron.demo.core.services.generation.CommonGenerator
import com.github.dannecron.demo.db.entity.Product
import com.github.dannecron.demo.db.repository.ProductRepository
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
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 com.github.dannecron.demo.utils.LoggerDelegate
import net.logstash.logback.marker.Markers
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import java.time.OffsetDateTime
import java.util.*
import org.springframework.stereotype.Service
import java.time.format.DateTimeFormatter
import java.util.UUID
@Service
class ProductServiceImpl(
private val defaultSyncTopic: String,
private val productRepository: ProductRepository,
private val producer: Producer,
private val commonGenerator: CommonGenerator,
): ProductService {
private val logger by LoggerDelegate()
@@ -32,11 +37,11 @@ class ProductServiceImpl(
override fun create(name: String, price: Long, description: String?): Product {
val product = Product(
id = null,
guid = UUID.randomUUID(),
guid = commonGenerator.generateUUID(),
name = name,
description = description,
price = price,
createdAt = OffsetDateTime.now(),
createdAt = commonGenerator.generateCurrentTime(),
updatedAt = null,
deletedAt = null,
)
@@ -52,7 +57,7 @@ class ProductServiceImpl(
}
val deletedProduct = product.copy(
deletedAt = OffsetDateTime.now(),
deletedAt = commonGenerator.generateCurrentTime(),
)
return productRepository.save(deletedProduct)
@@ -61,7 +66,17 @@ class ProductServiceImpl(
override fun syncToKafka(guid: UUID, topic: String?) {
val product = findByGuid(guid) ?: throw ProductNotFoundException()
producer.produceProductInfo(topic ?: defaultSyncTopic, product)
producer.produceProductSync(product.toKafkaDto())
}
private fun Product.toKafkaDto() = ProductDto(
id = id ?: throw InvalidArgumentException("product.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),
)
}

View File

@@ -1,14 +0,0 @@
package com.github.dannecron.demo.services.validation
import com.github.dannecron.demo.services.validation.exceptions.ElementNotValidException
import com.github.dannecron.demo.services.validation.exceptions.SchemaNotFoundException
import kotlinx.serialization.json.JsonElement
interface SchemaValidator {
companion object {
const val SCHEMA_KAFKA_PRODUCT_SYNC = "kafka-product-sync"
}
@Throws(ElementNotValidException::class, SchemaNotFoundException::class)
fun validate(schemaName: String, value: JsonElement)
}

View File

@@ -1,42 +0,0 @@
package com.github.dannecron.demo.services.validation
import com.github.dannecron.demo.services.validation.exceptions.ElementNotValidException
import com.github.dannecron.demo.services.validation.exceptions.SchemaNotFoundException
import io.github.optimumcode.json.schema.JsonSchema
import io.github.optimumcode.json.schema.ValidationError
import kotlinx.serialization.json.JsonElement
import org.springframework.util.ResourceUtils
class SchemaValidatorImp(
private val schemaMap: Map<String, String>,
): SchemaValidator {
private val loadedSchema: MutableMap<String, String> = mutableMapOf()
override fun validate(schemaName: String, value: JsonElement) {
JsonSchema.fromDefinition(
getSchema(schemaName),
).also {
val errors = mutableListOf<ValidationError>()
if (!it.validate(value, errors::add)) {
throw ElementNotValidException(errors)
}
}
}
private fun getSchema(schemaName: String): String {
val loaded = loadedSchema[schemaName]
if (loaded != null) {
return loaded
}
val schemaFile = schemaMap[schemaName]
?: throw SchemaNotFoundException()
val schema = ResourceUtils.getFile("classpath:json-schemas/$schemaFile")
.readText(Charsets.UTF_8)
loadedSchema[schemaName] = schema
return schema
}
}

View File

@@ -1,3 +0,0 @@
package com.github.dannecron.demo.services.validation.exceptions
class SchemaNotFoundException: RuntimeException()

View File

@@ -1,14 +0,0 @@
package com.github.dannecron.demo.utils
import kotlin.math.pow
import kotlin.math.roundToInt
fun Double.roundTo(numFractionDigits: Int): Double {
val factor = 10.0.pow(numFractionDigits.toDouble())
return (this * factor).roundToInt() / factor
}
fun String.snakeToCamelCase(): String {
val pattern = "_[a-z]".toRegex()
return replace(pattern) { it.value.last().uppercase() }
}

View File

@@ -1,19 +0,0 @@
package com.github.dannecron.demo
import com.github.dannecron.demo.services.kafka.Producer
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories
import org.springframework.test.context.ActiveProfiles
import org.testcontainers.junit.jupiter.Testcontainers
@ActiveProfiles("db")
@DataJdbcTest
@Testcontainers(disabledWithoutDocker = false)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@EnableJdbcRepositories
class BaseDbTest {
@MockBean
lateinit var producer: Producer
}

View File

@@ -1,56 +0,0 @@
package com.github.dannecron.demo.services.database.city
import com.github.dannecron.demo.BaseDbTest
import com.github.dannecron.demo.models.City
import com.github.dannecron.demo.providers.CityRepository
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import java.util.*
import kotlin.test.*
@ContextConfiguration(classes = [CityRepository::class, CityServiceImpl::class])
class CityServiceImplDbTest: BaseDbTest() {
@Autowired
private lateinit var cityRepository: CityRepository
@Autowired
private lateinit var cityServiceImpl: CityServiceImpl
@Test
fun createFindDelete_success() {
val name = "Some city"
var city: City? = null
try {
city = cityServiceImpl.create(name = name)
assertNotNull(city.id)
assertEquals(name, city.name)
val dbCity = cityServiceImpl.findByGuid(city.guid)
assertNotNull(dbCity)
assertEquals(city.id, dbCity.id)
assertFalse(dbCity.isDeleted())
val deletedCity = cityServiceImpl.delete(city.guid)
assertEquals(city.id, deletedCity.id)
assertNotNull(deletedCity.deletedAt)
assertTrue(deletedCity.isDeleted())
assertThrows<AlreadyDeletedException> {
cityServiceImpl.delete(city.guid)
}
assertThrows<CityNotFoundException> {
cityServiceImpl.delete(UUID.randomUUID())
}
} finally {
val id = city?.id
if (id != null) {
cityRepository.deleteById(id)
}
}
}
}

View File

@@ -0,0 +1,70 @@
package com.github.dannecron.demo.services.database.city
import com.github.dannecron.demo.core.services.generation.CommonGenerator
import com.github.dannecron.demo.db.entity.City
import com.github.dannecron.demo.db.repository.CityRepository
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
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.time.format.DateTimeFormatter
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
class CityServiceImplTest {
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 cityRepository: CityRepository = mock()
private val cityServiceImpl = CityServiceImpl(cityRepository, commonGenerator)
private val city = City(
id = 1000,
guid = mockGuid,
name = "name",
createdAt = mockCurrentTime,
updatedAt = null,
deletedAt = null,
)
@Test
fun `create - by name`() {
whenever(cityRepository.save(any<City>())).thenReturn(city)
val result = cityServiceImpl.create("name")
assertEquals(city, result)
verify(cityRepository, times(1)).save(city.copy(id = null))
}
@Test
fun `create - by dto`() {
val cityGuid = UUID.randomUUID()
val createdAt = OffsetDateTime.now()
val cityCreate = CityCreateDto(
guid = cityGuid.toString(),
name = "name",
createdAt = createdAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
updatedAt = null,
deletedAt = null,
)
val expectedCity = city.copy(guid = cityGuid, createdAt = createdAt)
whenever(cityRepository.save(any<City>())).thenReturn(expectedCity)
val result = cityServiceImpl.create(cityCreate)
assertEquals(expectedCity, result)
verify(cityRepository, times(1)).save(expectedCity.copy(id = null))
}
}

View File

@@ -1,74 +0,0 @@
package com.github.dannecron.demo.services.database.customer
import com.github.dannecron.demo.BaseDbTest
import com.github.dannecron.demo.models.City
import com.github.dannecron.demo.providers.CityRepository
import com.github.dannecron.demo.providers.CustomerRepository
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import java.time.OffsetDateTime
import java.util.*
import kotlin.test.*
@ContextConfiguration(classes = [CustomerRepository::class, CityRepository::class, CustomerServiceImpl::class])
class CustomerServiceImplDbTest: BaseDbTest() {
@Autowired
private lateinit var customerRepository: CustomerRepository
@Autowired
private lateinit var cityRepository: CityRepository
@Autowired
private lateinit var customerServiceImpl: CustomerServiceImpl
@Test
fun createFind_success() {
val nameOne = "Some Dude-One"
val nameTwo = "Some Dude-Two"
val nameThree = "Some Dude-Three"
var city = City(
id = null,
guid = UUID.randomUUID(),
name = "some city name",
createdAt = OffsetDateTime.now(),
updatedAt = null,
deletedAt = null,
)
var customerIds = longArrayOf()
try {
city = cityRepository.save(city)
customerServiceImpl.create(nameTwo, null).let {
customerIds += it.id ?: fail("customerWithNoCity id is null")
assertNull(it.cityId)
assertNotNull(it.createdAt)
assertNull(it.updatedAt)
}
val customerWithCity = customerServiceImpl.create(nameOne, city.guid)
customerIds += customerWithCity.id ?: fail("customerWithCity id is null")
assertEquals(city.id, customerWithCity.cityId)
assertNotNull(customerWithCity.createdAt)
assertNull(customerWithCity.updatedAt)
val existedCustomer = customerServiceImpl.findByGuid(customerWithCity.guid)
assertNotNull(existedCustomer)
assertEquals(customerWithCity.id, existedCustomer.customer.id)
assertEquals(city.id, existedCustomer.city?.id)
assertThrows<CityNotFoundException> {
customerServiceImpl.create(nameThree, UUID.randomUUID())
}
} finally {
val cityId = city.id
if (cityId != null) {
cityRepository.deleteById(cityId)
}
customerIds.onEach { customerRepository.deleteById(it) }
}
}
}

View File

@@ -0,0 +1,124 @@
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)
}
}

View File

@@ -1,83 +1,147 @@
package com.github.dannecron.demo.services.database.order
import com.github.dannecron.demo.BaseDbTest
import com.github.dannecron.demo.models.Customer
import com.github.dannecron.demo.models.Product
import com.github.dannecron.demo.models.order.Order
import com.github.dannecron.demo.providers.CustomerRepository
import com.github.dannecron.demo.providers.OrderRepository
import com.github.dannecron.demo.providers.ProductRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
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.*
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@ContextConfiguration(classes = [OrderServiceImpl::class])
class OrderServiceImplTest: BaseDbTest() {
@Autowired
private lateinit var orderRepository: OrderRepository
@Autowired
private lateinit var productRepository: ProductRepository
@Autowired
private lateinit var customerRepository: CustomerRepository
@Autowired
private lateinit var orderServiceImpl: OrderServiceImpl
class OrderServiceImplTest {
private val mockGuid = UUID.randomUUID()
private val mockCurrentTime = OffsetDateTime.now()
@Test
fun createAndFind_success() {
var productOneId: Long? = null
var productTwoId: Long? = null
var customerId: Long? = null
var order: Order? = null
try {
val productOne = makeProduct().let { productRepository.save(it) }.also { productOneId = it.id!! }
val productTwo = makeProduct(price = 20000L)
.let { productRepository.save(it) }
.also { productTwoId = it.id!! }
val customer = makeCustomer().let { customerRepository.save(it) }.also { customerId = it.id!! }
order = orderServiceImpl.createOrder(customer, setOf(productOne, productTwo))
assertNotNull(order.id)
val orderWithProducts = orderServiceImpl.getByCustomerId(customerId = customerId!!)
assertEquals(1, orderWithProducts.count())
orderWithProducts.first().also {
assertEquals(order.id, it.order.id)
assertEquals(2, it.products.count())
assertEquals(productTwo.id, it.getMostExpensiveOrderedProduct()?.id)
assertEquals(300.00, it.getTotalOrderPrice())
}
} finally {
order?.id?.also { orderRepository.deleteById(it) }
customerId?.also { customerRepository.deleteById(it) }
productOneId?.also { productRepository.deleteById(it) }
productTwoId?.also { productRepository.deleteById(it) }
}
private val commonGenerator: CommonGenerator = mock {
on { generateUUID() } doReturn mockGuid
on { generateCurrentTime() } doReturn mockCurrentTime
}
private fun makeProduct(price: Long = 10000L): Product = Product(
id = null,
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 = "name" + 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 = price,
createdAt = OffsetDateTime.now(),
price = 10000L,
createdAt = now.minusMonths(1),
updatedAt = null,
deletedAt = null,
)
private fun makeCustomer(): Customer = Customer(
id = null,
private val orderProduct = OrderProduct(
guid = UUID.randomUUID(),
name = "client",
cityId = null,
createdAt = OffsetDateTime.now(),
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))
}
}

View File

@@ -1,69 +0,0 @@
package com.github.dannecron.demo.services.database.product
import com.github.dannecron.demo.BaseDbTest
import com.github.dannecron.demo.models.Product
import com.github.dannecron.demo.providers.ProductRepository
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.test.context.ContextConfiguration
import java.util.*
import kotlin.test.*
@ContextConfiguration(classes = [ProductRepository::class])
class ProductServiceImplDbTest: BaseDbTest() {
private lateinit var productService: ProductServiceImpl
@Autowired
private lateinit var productRepository: ProductRepository
@BeforeTest
fun setUp() {
productService = ProductServiceImpl(
defaultSyncTopic = "some-default-topic",
productRepository = productRepository,
producer = producer
)
}
@Test
fun createFindDelete_success() {
val name = "new-product-name"
val price = 33333L
val description = "some-description"
var product: Product? = null
try {
product = productService.create(name = name, price = price, description = description)
assertNotNull(product.id)
assertEquals(name, product.name)
assertEquals(price, product.price)
assertEquals(333.33, product.getPriceDouble())
val dbProduct = productService.findByGuid(product.guid)
assertNotNull(dbProduct)
assertEquals(product.id, dbProduct.id)
assertFalse(dbProduct.isDeleted())
val deletedProduct = productService.delete(product.guid)
assertNotNull(deletedProduct)
assertEquals(product.id, deletedProduct.id)
assertNotNull(deletedProduct.deletedAt)
assertTrue(deletedProduct.isDeleted())
// try to delete already deleted product
assertThrows<AlreadyDeletedException> {
productService.delete(product.guid)
}
assertThrows<ProductNotFoundException> {
productService.delete(UUID.randomUUID())
}
} finally {
val id = product?.id
if (id != null) {
productRepository.deleteById(id)
}
}
}
}

View File

@@ -1,99 +1,149 @@
package com.github.dannecron.demo.services.database.product
import com.github.dannecron.demo.BaseUnitTest
import com.github.dannecron.demo.models.Product
import com.github.dannecron.demo.providers.ProductRepository
import com.github.dannecron.demo.core.services.generation.CommonGenerator
import com.github.dannecron.demo.db.entity.Product
import com.github.dannecron.demo.db.repository.ProductRepository
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
import com.github.dannecron.demo.services.kafka.Producer
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
import com.github.dannecron.demo.services.kafka.dto.ProductDto
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.runner.RunWith
import org.mockito.kotlin.*
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.test.context.junit4.SpringRunner
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.*
import kotlin.test.BeforeTest
import kotlin.test.Test
import java.time.format.DateTimeFormatter
import java.util.UUID
import kotlin.test.assertEquals
@RunWith(SpringRunner::class)
@SpringBootTest
class ProductServiceImplTest: BaseUnitTest() {
private val defaultTopic = "some-default-topic"
private lateinit var productService: ProductServiceImpl
class ProductServiceImplTest {
private val mockGuid = UUID.randomUUID()
private val mockCurrentTime = OffsetDateTime.now()
@MockBean
@Qualifier("producer")
private lateinit var producer: Producer
@MockBean
private lateinit var productRepository: ProductRepository
@BeforeTest
fun setUp() {
productService = ProductServiceImpl(
defaultSyncTopic = defaultTopic,
productRepository = productRepository,
producer = producer,
)
private val productRepository: ProductRepository = mock()
private val producer: Producer = mock()
private val commonGenerator: CommonGenerator = mock {
on { generateUUID() } doReturn mockGuid
on { generateCurrentTime() } doReturn mockCurrentTime
}
private val productService = ProductServiceImpl(
productRepository = productRepository,
producer = producer,
commonGenerator = commonGenerator,
)
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() {
val guid = UUID.randomUUID()
val product = Product(
id = 123,
guid = guid,
fun create() {
val expectedProductForCreation = product.copy(
id = null,
guid = mockGuid,
createdAt = mockCurrentTime,
updatedAt = null,
)
val expectedCreatedProduct = expectedProductForCreation.copy(id = 1)
whenever(productRepository.save<Product>(any())).thenReturn(expectedCreatedProduct)
val result = productService.create(
name = "name",
description = "description",
price = 10050,
createdAt = OffsetDateTime.now().minusDays(1),
updatedAt = OffsetDateTime.now().minusHours(2),
deletedAt = OffsetDateTime.now(),
description = "description",
)
assertEquals(expectedCreatedProduct, result)
whenever(productRepository.findByGuid(eq(guid))) doReturn product
whenever(producer.produceProductInfo(defaultTopic, product)) doAnswer {}
productService.syncToKafka(guid, null)
verify(productRepository, times(1)).save(expectedProductForCreation)
}
@Test
fun syncToKafka_notFound() {
val specificTopic = "specificNotice"
val guid = UUID.randomUUID()
fun `delete - success`() {
val deletedProduct = product.copy(
deletedAt = mockCurrentTime,
)
whenever(productRepository.findByGuid(any())).thenReturn(product)
whenever(productRepository.save<Product>(any())).thenReturn(deletedProduct)
whenever(productRepository.findByGuid(eq(guid))) doReturn null
val result = productService.delete(guid)
assertEquals(deletedProduct, result)
verify(productRepository, times(1)).findByGuid(guid)
verify(productRepository, times(1)).save(deletedProduct)
}
@Test
fun `delete - fail - already deleted`() {
val deletedProduct = product.copy(
deletedAt = mockCurrentTime,
)
whenever(productRepository.findByGuid(any())).thenReturn(deletedProduct)
assertThrows<AlreadyDeletedException> {
productService.delete(guid)
}
verify(productRepository, times(1)).findByGuid(guid)
verify(productRepository, never()).save(any())
}
@Test
fun `delete - fail - not found`() {
whenever(productRepository.findByGuid(any())).thenReturn(null)
assertThrows<ProductNotFoundException> {
productService.syncToKafka(guid, specificTopic)
productService.delete(guid)
}
verifyNoInteractions(producer)
verify(productRepository, times(1)).findByGuid(guid)
verify(productRepository, never()).save(any())
}
@Test
fun syncToKafka_invalidArgumentException() {
val specificTopic = "specificNotice"
val guid = UUID.randomUUID()
fun `syncToKafka - success`() {
whenever(productRepository.findByGuid(any())) doReturn product
val product = Product(
id = 123,
guid = guid,
name = "name",
description = "description",
price = 10050,
createdAt = OffsetDateTime.now().minusDays(1),
updatedAt = OffsetDateTime.now().minusHours(2),
deletedAt = OffsetDateTime.now(),
)
productService.syncToKafka(guid, null)
whenever(productRepository.findByGuid(eq(guid))) doReturn product
whenever(producer.produceProductInfo(specificTopic, product)) doThrow InvalidArgumentException("some error")
verify(productRepository, times(1)).findByGuid(guid)
verify(producer, times(1)).produceProductSync(kafkaProductDto)
}
assertThrows< InvalidArgumentException> {
productService.syncToKafka(guid, specificTopic)
@Test
fun `syncToKafka - not found`() {
whenever(productRepository.findByGuid(any())) doReturn null
assertThrows<ProductNotFoundException> {
productService.syncToKafka(guid, null)
}
verify(productRepository, times(1)).findByGuid(guid)
verifyNoInteractions(producer)
}
}