diff --git a/build.gradle.kts b/build.gradle.kts index d1fb504..47bd99c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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) } diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..674b9a2 --- /dev/null +++ b/core/build.gradle.kts @@ -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) +} diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/config/SchemaValidationConfig.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/config/SchemaValidationConfig.kt new file mode 100644 index 0000000..d6f0304 --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/config/SchemaValidationConfig.kt @@ -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) + } + ) +} diff --git a/src/main/kotlin/com/github/dannecron/demo/config/properties/ValidationProperties.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/config/properties/ValidationProperties.kt similarity index 76% rename from src/main/kotlin/com/github/dannecron/demo/config/properties/ValidationProperties.kt rename to core/src/main/kotlin/com/github/dannecron/demo/core/config/properties/ValidationProperties.kt index 54698da..f51b506 100644 --- a/src/main/kotlin/com/github/dannecron/demo/config/properties/ValidationProperties.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/config/properties/ValidationProperties.kt @@ -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 diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/services/generation/CommonGenerator.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/services/generation/CommonGenerator.kt new file mode 100644 index 0000000..988d64e --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/services/generation/CommonGenerator.kt @@ -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 +} diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/services/generation/CommonGeneratorImpl.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/services/generation/CommonGeneratorImpl.kt new file mode 100644 index 0000000..de088a8 --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/services/generation/CommonGeneratorImpl.kt @@ -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() + } +} diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/services/validation/SchemaValidator.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/services/validation/SchemaValidator.kt new file mode 100644 index 0000000..2444ee3 --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/services/validation/SchemaValidator.kt @@ -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) +} diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/services/validation/SchemaValidatorImp.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/services/validation/SchemaValidatorImp.kt new file mode 100644 index 0000000..05aaba9 --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/services/validation/SchemaValidatorImp.kt @@ -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, +): SchemaValidator { + + @Throws( + SchemaNotFoundException::class, + ElementNotValidException::class, + ) + override fun validate(schemaName: String, value: JsonElement) { + JsonSchema.fromDefinition( + getSchema(schemaName), + ).also { + val errors = mutableListOf() + + if (!it.validate(value, errors::add)) { + throw ElementNotValidException(errors) + } + } + } + + @Throws(SchemaNotFoundException::class) + private fun getSchema(schemaName: String) = schemaMap[schemaName] + ?: throw SchemaNotFoundException() +} diff --git a/src/main/kotlin/com/github/dannecron/demo/services/validation/exceptions/ElementNotValidException.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/services/validation/exceptions/ElementNotValidException.kt similarity index 69% rename from src/main/kotlin/com/github/dannecron/demo/services/validation/exceptions/ElementNotValidException.kt rename to core/src/main/kotlin/com/github/dannecron/demo/core/services/validation/exceptions/ElementNotValidException.kt index e563994..7ac9e18 100644 --- a/src/main/kotlin/com/github/dannecron/demo/services/validation/exceptions/ElementNotValidException.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/services/validation/exceptions/ElementNotValidException.kt @@ -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 diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/services/validation/exceptions/SchemaNotFoundException.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/services/validation/exceptions/SchemaNotFoundException.kt new file mode 100644 index 0000000..cc90b83 --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/services/validation/exceptions/SchemaNotFoundException.kt @@ -0,0 +1,3 @@ +package com.github.dannecron.demo.core.services.validation.exceptions + +class SchemaNotFoundException: RuntimeException() diff --git a/src/main/resources/json-schemas/kafka/product/sync.json b/core/src/main/resources/json-schemas/kafka/product/sync.json similarity index 100% rename from src/main/resources/json-schemas/kafka/product/sync.json rename to core/src/main/resources/json-schemas/kafka/product/sync.json diff --git a/src/test/kotlin/com/github/dannecron/demo/services/validation/SchemaValidatorImpTest.kt b/core/src/test/kotlin/com/github/dannecron/demo/core/services/validation/SchemaValidatorImpTest.kt similarity index 71% rename from src/test/kotlin/com/github/dannecron/demo/services/validation/SchemaValidatorImpTest.kt rename to core/src/test/kotlin/com/github/dannecron/demo/core/services/validation/SchemaValidatorImpTest.kt index bafdc63..3b4f7d2 100644 --- a/src/test/kotlin/com/github/dannecron/demo/services/validation/SchemaValidatorImpTest.kt +++ b/core/src/test/kotlin/com/github/dannecron/demo/core/services/validation/SchemaValidatorImpTest.kt @@ -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?) { @@ -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) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fde85b5..d7f63be 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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"} diff --git a/settings.gradle.kts b/settings.gradle.kts index 2cbfc2f..556f685 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,6 @@ +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} rootProject.name = "demo" include("db") +include("core") diff --git a/src/main/kotlin/com/github/dannecron/demo/models/City.kt b/src/main/kotlin/com/github/dannecron/demo/models/City.kt deleted file mode 100644 index c9a7ef6..0000000 --- a/src/main/kotlin/com/github/dannecron/demo/models/City.kt +++ /dev/null @@ -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 -} diff --git a/src/main/kotlin/com/github/dannecron/demo/models/Customer.kt b/src/main/kotlin/com/github/dannecron/demo/models/Customer.kt deleted file mode 100644 index 02d19ba..0000000 --- a/src/main/kotlin/com/github/dannecron/demo/models/Customer.kt +++ /dev/null @@ -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?, -) diff --git a/src/main/kotlin/com/github/dannecron/demo/models/CustomerExtended.kt b/src/main/kotlin/com/github/dannecron/demo/models/CustomerExtended.kt index 7ca4858..c11bdb5 100644 --- a/src/main/kotlin/com/github/dannecron/demo/models/CustomerExtended.kt +++ b/src/main/kotlin/com/github/dannecron/demo/models/CustomerExtended.kt @@ -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 diff --git a/src/main/kotlin/com/github/dannecron/demo/models/order/OrderWithProducts.kt b/src/main/kotlin/com/github/dannecron/demo/models/OrderWithProducts.kt similarity index 64% rename from src/main/kotlin/com/github/dannecron/demo/models/order/OrderWithProducts.kt rename to src/main/kotlin/com/github/dannecron/demo/models/OrderWithProducts.kt index c38cf38..86796b7 100644 --- a/src/main/kotlin/com/github/dannecron/demo/models/order/OrderWithProducts.kt +++ b/src/main/kotlin/com/github/dannecron/demo/models/OrderWithProducts.kt @@ -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, diff --git a/src/main/kotlin/com/github/dannecron/demo/models/Product.kt b/src/main/kotlin/com/github/dannecron/demo/models/Product.kt deleted file mode 100644 index a4dc8bc..0000000 --- a/src/main/kotlin/com/github/dannecron/demo/models/Product.kt +++ /dev/null @@ -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 -} diff --git a/src/main/kotlin/com/github/dannecron/demo/models/order/Order.kt b/src/main/kotlin/com/github/dannecron/demo/models/order/Order.kt deleted file mode 100644 index 7b9a57e..0000000 --- a/src/main/kotlin/com/github/dannecron/demo/models/order/Order.kt +++ /dev/null @@ -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 -} diff --git a/src/main/kotlin/com/github/dannecron/demo/models/order/OrderProduct.kt b/src/main/kotlin/com/github/dannecron/demo/models/order/OrderProduct.kt deleted file mode 100644 index 334d072..0000000 --- a/src/main/kotlin/com/github/dannecron/demo/models/order/OrderProduct.kt +++ /dev/null @@ -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 { - @Transient - var isNewInstance: Boolean? = null - - override fun getId(): UUID { - return guid - } - - override fun isNew(): Boolean { - return isNewInstance ?: true - } -} diff --git a/src/main/kotlin/com/github/dannecron/demo/models/serializables/OffsetDateTimeSerialization.kt b/src/main/kotlin/com/github/dannecron/demo/models/serializables/OffsetDateTimeSerialization.kt deleted file mode 100644 index 46bed04..0000000 --- a/src/main/kotlin/com/github/dannecron/demo/models/serializables/OffsetDateTimeSerialization.kt +++ /dev/null @@ -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 { - 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)) - } -} diff --git a/src/main/kotlin/com/github/dannecron/demo/models/serializables/UuidSerialization.kt b/src/main/kotlin/com/github/dannecron/demo/models/serializables/UuidSerialization.kt deleted file mode 100644 index a694fe0..0000000 --- a/src/main/kotlin/com/github/dannecron/demo/models/serializables/UuidSerialization.kt +++ /dev/null @@ -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 { - 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()) - } -} diff --git a/src/main/kotlin/com/github/dannecron/demo/services/database/city/CityService.kt b/src/main/kotlin/com/github/dannecron/demo/services/database/city/CityService.kt index 55cbfc1..ff34ed4 100644 --- a/src/main/kotlin/com/github/dannecron/demo/services/database/city/CityService.kt +++ b/src/main/kotlin/com/github/dannecron/demo/services/database/city/CityService.kt @@ -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 diff --git a/src/main/kotlin/com/github/dannecron/demo/services/database/city/CityServiceImpl.kt b/src/main/kotlin/com/github/dannecron/demo/services/database/city/CityServiceImpl.kt index 60d5ec5..95c683e 100644 --- a/src/main/kotlin/com/github/dannecron/demo/services/database/city/CityServiceImpl.kt +++ b/src/main/kotlin/com/github/dannecron/demo/services/database/city/CityServiceImpl.kt @@ -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 { diff --git a/src/main/kotlin/com/github/dannecron/demo/services/database/customer/CustomerService.kt b/src/main/kotlin/com/github/dannecron/demo/services/database/customer/CustomerService.kt index 89c6e4a..60c9f9b 100644 --- a/src/main/kotlin/com/github/dannecron/demo/services/database/customer/CustomerService.kt +++ b/src/main/kotlin/com/github/dannecron/demo/services/database/customer/CustomerService.kt @@ -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.* diff --git a/src/main/kotlin/com/github/dannecron/demo/services/database/customer/CustomerServiceImpl.kt b/src/main/kotlin/com/github/dannecron/demo/services/database/customer/CustomerServiceImpl.kt index 4ead70c..5be5c22 100644 --- a/src/main/kotlin/com/github/dannecron/demo/services/database/customer/CustomerServiceImpl.kt +++ b/src/main/kotlin/com/github/dannecron/demo/services/database/customer/CustomerServiceImpl.kt @@ -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) diff --git a/src/main/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImpl.kt b/src/main/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImpl.kt index a54cab9..7f8c049 100644 --- a/src/main/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImpl.kt +++ b/src/main/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImpl.kt @@ -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 = orderRepository.findByCustomerId(customerId) + fun findByCustomerId(customerId: Long): List = 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): 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 = + orderProductRepository.findByOrderId(orderId = orderId) + .map { it.productId } + .let { + if (it.isEmpty()) { + emptyList() + } else { + productRepository.findAllById(it).toList() + } } + + private fun saveProductsForNewOrder(savedOrder: Order, products: List) { + products.map { + OrderProduct( + guid = commonGenerator.generateUUID(), + orderId = savedOrder.id!!, + productId = it.id!!, + createdAt = commonGenerator.generateCurrentTime(), + updatedAt = null + ) + }.also { orderProductRepository.saveAll(it) } } } diff --git a/src/main/kotlin/com/github/dannecron/demo/services/database/product/ProductService.kt b/src/main/kotlin/com/github/dannecron/demo/services/database/product/ProductService.kt index 15bcc1d..5fdd457 100644 --- a/src/main/kotlin/com/github/dannecron/demo/services/database/product/ProductService.kt +++ b/src/main/kotlin/com/github/dannecron/demo/services/database/product/ProductService.kt @@ -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 { diff --git a/src/main/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImpl.kt b/src/main/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImpl.kt index 80dcffb..379e635 100644 --- a/src/main/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImpl.kt +++ b/src/main/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImpl.kt @@ -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), + ) } diff --git a/src/main/kotlin/com/github/dannecron/demo/services/validation/SchemaValidator.kt b/src/main/kotlin/com/github/dannecron/demo/services/validation/SchemaValidator.kt deleted file mode 100644 index 44f353a..0000000 --- a/src/main/kotlin/com/github/dannecron/demo/services/validation/SchemaValidator.kt +++ /dev/null @@ -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) -} diff --git a/src/main/kotlin/com/github/dannecron/demo/services/validation/SchemaValidatorImp.kt b/src/main/kotlin/com/github/dannecron/demo/services/validation/SchemaValidatorImp.kt deleted file mode 100644 index 3ae174d..0000000 --- a/src/main/kotlin/com/github/dannecron/demo/services/validation/SchemaValidatorImp.kt +++ /dev/null @@ -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, -): SchemaValidator { - private val loadedSchema: MutableMap = mutableMapOf() - - override fun validate(schemaName: String, value: JsonElement) { - JsonSchema.fromDefinition( - getSchema(schemaName), - ).also { - val errors = mutableListOf() - - 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 - } -} diff --git a/src/main/kotlin/com/github/dannecron/demo/services/validation/exceptions/SchemaNotFoundException.kt b/src/main/kotlin/com/github/dannecron/demo/services/validation/exceptions/SchemaNotFoundException.kt deleted file mode 100644 index 76f9fe7..0000000 --- a/src/main/kotlin/com/github/dannecron/demo/services/validation/exceptions/SchemaNotFoundException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.github.dannecron.demo.services.validation.exceptions - -class SchemaNotFoundException: RuntimeException() diff --git a/src/main/kotlin/com/github/dannecron/demo/utils/Extensions.kt b/src/main/kotlin/com/github/dannecron/demo/utils/Extensions.kt deleted file mode 100644 index 0022e8d..0000000 --- a/src/main/kotlin/com/github/dannecron/demo/utils/Extensions.kt +++ /dev/null @@ -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() } -} diff --git a/src/test/kotlin/com/github/dannecron/demo/BaseDbTest.kt b/src/test/kotlin/com/github/dannecron/demo/BaseDbTest.kt deleted file mode 100644 index 49f3ce1..0000000 --- a/src/test/kotlin/com/github/dannecron/demo/BaseDbTest.kt +++ /dev/null @@ -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 -} diff --git a/src/test/kotlin/com/github/dannecron/demo/services/database/city/CityServiceImplDbTest.kt b/src/test/kotlin/com/github/dannecron/demo/services/database/city/CityServiceImplDbTest.kt deleted file mode 100644 index 64c51b4..0000000 --- a/src/test/kotlin/com/github/dannecron/demo/services/database/city/CityServiceImplDbTest.kt +++ /dev/null @@ -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 { - cityServiceImpl.delete(city.guid) - } - - assertThrows { - cityServiceImpl.delete(UUID.randomUUID()) - } - } finally { - val id = city?.id - if (id != null) { - cityRepository.deleteById(id) - } - - } - } -} diff --git a/src/test/kotlin/com/github/dannecron/demo/services/database/city/CityServiceImplTest.kt b/src/test/kotlin/com/github/dannecron/demo/services/database/city/CityServiceImplTest.kt new file mode 100644 index 0000000..bbd3c73 --- /dev/null +++ b/src/test/kotlin/com/github/dannecron/demo/services/database/city/CityServiceImplTest.kt @@ -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())).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())).thenReturn(expectedCity) + + val result = cityServiceImpl.create(cityCreate) + assertEquals(expectedCity, result) + + verify(cityRepository, times(1)).save(expectedCity.copy(id = null)) + } +} diff --git a/src/test/kotlin/com/github/dannecron/demo/services/database/customer/CustomerServiceImplDbTest.kt b/src/test/kotlin/com/github/dannecron/demo/services/database/customer/CustomerServiceImplDbTest.kt deleted file mode 100644 index 61aa36e..0000000 --- a/src/test/kotlin/com/github/dannecron/demo/services/database/customer/CustomerServiceImplDbTest.kt +++ /dev/null @@ -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 { - customerServiceImpl.create(nameThree, UUID.randomUUID()) - } - } finally { - val cityId = city.id - if (cityId != null) { - cityRepository.deleteById(cityId) - } - - customerIds.onEach { customerRepository.deleteById(it) } - } - } -} diff --git a/src/test/kotlin/com/github/dannecron/demo/services/database/customer/CustomerServiceImplTest.kt b/src/test/kotlin/com/github/dannecron/demo/services/database/customer/CustomerServiceImplTest.kt new file mode 100644 index 0000000..90f6bb6 --- /dev/null +++ b/src/test/kotlin/com/github/dannecron/demo/services/database/customer/CustomerServiceImplTest.kt @@ -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())).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())).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())).thenReturn(customer) + whenever(cityRepository.findByGuid(cityGuid)).thenReturn(null) + + assertThrows { + 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) + } +} diff --git a/src/test/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImplTest.kt b/src/test/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImplTest.kt index c035241..028f196 100644 --- a/src/test/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImplTest.kt +++ b/src/test/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImplTest.kt @@ -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())).thenReturn(newOrder) + whenever(orderProductRepository.saveAll(any>())).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)) + } } diff --git a/src/test/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImplDbTest.kt b/src/test/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImplDbTest.kt deleted file mode 100644 index b0cf373..0000000 --- a/src/test/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImplDbTest.kt +++ /dev/null @@ -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 { - productService.delete(product.guid) - } - - assertThrows { - productService.delete(UUID.randomUUID()) - } - } finally { - val id = product?.id - if (id != null) { - productRepository.deleteById(id) - } - } - } -} diff --git a/src/test/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImplTest.kt b/src/test/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImplTest.kt index 320f733..5b563ea 100644 --- a/src/test/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImplTest.kt +++ b/src/test/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImplTest.kt @@ -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(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(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 { + 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 { - 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 { + productService.syncToKafka(guid, null) } + + verify(productRepository, times(1)).findByGuid(guid) + verifyNoInteractions(producer) } }