mirror of
https://github.com/Dannecron/spring-boot-demo.git
synced 2025-12-25 16:22:35 +03:00
move validation to separate core package, add generation service
some code cleanup
This commit is contained in:
@@ -69,13 +69,13 @@ subprojects {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":db"))
|
implementation(project(":db"))
|
||||||
|
implementation(project(":core"))
|
||||||
|
|
||||||
runtimeOnly(libs.micrometer.registry.prometheus)
|
runtimeOnly(libs.micrometer.registry.prometheus)
|
||||||
|
|
||||||
implementation(libs.bundles.tracing)
|
implementation(libs.bundles.tracing)
|
||||||
implementation(libs.jackson.datatype.jsr)
|
implementation(libs.jackson.datatype.jsr)
|
||||||
implementation(libs.jackson.module.kotlin)
|
implementation(libs.jackson.module.kotlin)
|
||||||
implementation(libs.json.schema.validator)
|
|
||||||
implementation(libs.ktor.client.cio)
|
implementation(libs.ktor.client.cio)
|
||||||
implementation(libs.ktor.client.core)
|
implementation(libs.ktor.client.core)
|
||||||
implementation(libs.logback.encoder)
|
implementation(libs.logback.encoder)
|
||||||
@@ -85,13 +85,14 @@ dependencies {
|
|||||||
implementation(libs.spring.boot.starter.mustache)
|
implementation(libs.spring.boot.starter.mustache)
|
||||||
implementation(libs.spring.boot.starter.validation)
|
implementation(libs.spring.boot.starter.validation)
|
||||||
implementation(libs.spring.boot.starter.web)
|
implementation(libs.spring.boot.starter.web)
|
||||||
|
implementation(libs.spring.cloud.starter.streamKafka)
|
||||||
implementation(libs.spring.doc.openapi.starter)
|
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)
|
||||||
testImplementation(libs.testcontainers.junit.jupiter)
|
testImplementation(libs.testcontainers.junit.jupiter)
|
||||||
testImplementation(libs.ktor.client.mock)
|
|
||||||
|
|
||||||
developmentOnly(libs.spring.boot.devtools)
|
developmentOnly(libs.spring.boot.devtools)
|
||||||
}
|
}
|
||||||
|
|||||||
7
core/build.gradle.kts
Normal file
7
core/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
import io.github.optimumcode.json.schema.ValidationError
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.github.dannecron.demo.core.services.validation.exceptions
|
||||||
|
|
||||||
|
class SchemaNotFoundException: RuntimeException()
|
||||||
@@ -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.core.services.validation.exceptions.ElementNotValidException
|
||||||
import com.github.dannecron.demo.services.validation.SchemaValidator.Companion.SCHEMA_KAFKA_PRODUCT_SYNC
|
import com.github.dannecron.demo.core.services.validation.exceptions.SchemaNotFoundException
|
||||||
import com.github.dannecron.demo.services.validation.exceptions.ElementNotValidException
|
|
||||||
import com.github.dannecron.demo.services.validation.exceptions.SchemaNotFoundException
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.Arguments
|
import org.junit.jupiter.params.provider.Arguments
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
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.reflect.KClass
|
||||||
import kotlin.test.assertFailsWith
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
@RunWith(SpringRunner::class)
|
class SchemaValidatorImpTest {
|
||||||
@SpringBootTest
|
private val schemaValidatorImp = SchemaValidatorImp(
|
||||||
class SchemaValidatorImpTest(
|
schemaMap = mapOf(
|
||||||
@Autowired val schemaValidatorImp: SchemaValidatorImp
|
KAFKA_PRODUCT_SYNC_SCHEMA to getJsonSchema("json-schemas/kafka/product/sync.json")),
|
||||||
): BaseUnitTest() {
|
)
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("validateDataProvider")
|
@MethodSource("validateDataProvider")
|
||||||
fun validate(schemaName: String, inputRawJson: String, expectedException: KClass<out Throwable>?) {
|
fun validate(schemaName: String, inputRawJson: String, expectedException: KClass<out Throwable>?) {
|
||||||
@@ -27,8 +22,6 @@ class SchemaValidatorImpTest(
|
|||||||
|
|
||||||
if (expectedException == null) {
|
if (expectedException == null) {
|
||||||
schemaValidatorImp.validate(schemaName = schemaName, value = element)
|
schemaValidatorImp.validate(schemaName = schemaName, value = element)
|
||||||
// second time should use cache
|
|
||||||
schemaValidatorImp.validate(schemaName = schemaName, value = element)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -39,10 +32,12 @@ class SchemaValidatorImpTest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val KAFKA_PRODUCT_SYNC_SCHEMA = "kafkaProductSync"
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun validateDataProvider() = listOf(
|
fun validateDataProvider() = listOf(
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
SCHEMA_KAFKA_PRODUCT_SYNC,
|
KAFKA_PRODUCT_SYNC_SCHEMA,
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"id": 123,
|
"id": 123,
|
||||||
@@ -58,7 +53,7 @@ class SchemaValidatorImpTest(
|
|||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
Arguments.of( // no id
|
Arguments.of( // no id
|
||||||
SCHEMA_KAFKA_PRODUCT_SYNC,
|
KAFKA_PRODUCT_SYNC_SCHEMA,
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"guid": "3a27e322-b5b6-427f-b761-a02284c1cfa4",
|
"guid": "3a27e322-b5b6-427f-b761-a02284c1cfa4",
|
||||||
@@ -73,7 +68,7 @@ class SchemaValidatorImpTest(
|
|||||||
ElementNotValidException::class,
|
ElementNotValidException::class,
|
||||||
),
|
),
|
||||||
Arguments.of( // wrong guid
|
Arguments.of( // wrong guid
|
||||||
SCHEMA_KAFKA_PRODUCT_SYNC,
|
KAFKA_PRODUCT_SYNC_SCHEMA,
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"id": 213,
|
"id": 213,
|
||||||
@@ -95,4 +90,9 @@ class SchemaValidatorImpTest(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getJsonSchema(resourcePath: String) = javaClass.classLoader
|
||||||
|
.getResourceAsStream(resourcePath)!!
|
||||||
|
.readAllBytes()
|
||||||
|
.toString(Charsets.UTF_8)
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ jackson = "2.15.4"
|
|||||||
kotlin = "2.1.10"
|
kotlin = "2.1.10"
|
||||||
ktor = "3.0.0"
|
ktor = "3.0.0"
|
||||||
spring-boot = "3.2.10"
|
spring-boot = "3.2.10"
|
||||||
spring-kafka = "3.1.3"
|
spring-cloud = "3.2.10"
|
||||||
testcontainers = "1.19.7"
|
testcontainers = "1.19.7"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -26,14 +26,15 @@ postgres = { module = "org.postgresql:postgresql", version = "42.6.2" }
|
|||||||
spring-aspects = { module = "org.springframework:spring-aspects" }
|
spring-aspects = { module = "org.springframework:spring-aspects" }
|
||||||
spring-boot-devtools = { module = "org.springframework.boot:spring-boot-devtools" }
|
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-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-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-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-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-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-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-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 = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers"}
|
||||||
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers"}
|
testcontainers-junit-jupiter = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers"}
|
||||||
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers"}
|
testcontainers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "testcontainers"}
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
||||||
|
}
|
||||||
rootProject.name = "demo"
|
rootProject.name = "demo"
|
||||||
include("db")
|
include("db")
|
||||||
|
include("core")
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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?,
|
|
||||||
)
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.github.dannecron.demo.models
|
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
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -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(
|
data class OrderWithProducts(
|
||||||
val order: Order,
|
val order: Order,
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.github.dannecron.demo.services.database.city
|
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.CityNotFoundException
|
||||||
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
|
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
|
||||||
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
package com.github.dannecron.demo.services.database.city
|
package com.github.dannecron.demo.services.database.city
|
||||||
|
|
||||||
import com.github.dannecron.demo.models.City
|
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
||||||
import com.github.dannecron.demo.providers.CityRepository
|
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.AlreadyDeletedException
|
||||||
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
|
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
|
||||||
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Service
|
||||||
class CityServiceImpl(
|
class CityServiceImpl(
|
||||||
private val cityRepository: CityRepository
|
private val cityRepository: CityRepository,
|
||||||
|
private val commonGenerator: CommonGenerator,
|
||||||
): CityService {
|
): CityService {
|
||||||
override fun findByGuid(guid: UUID): City? = cityRepository.findByGuid(guid)
|
override fun findByGuid(guid: UUID): City? = cityRepository.findByGuid(guid)
|
||||||
|
|
||||||
override fun create(name: String): City = City(
|
override fun create(name: String): City = City(
|
||||||
id = null,
|
id = null,
|
||||||
guid = UUID.randomUUID(),
|
guid = commonGenerator.generateUUID(),
|
||||||
name = name,
|
name = name,
|
||||||
createdAt = OffsetDateTime.now(),
|
createdAt = commonGenerator.generateCurrentTime(),
|
||||||
updatedAt = null,
|
updatedAt = null,
|
||||||
deletedAt = null,
|
deletedAt = null,
|
||||||
).let {
|
).let {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.github.dannecron.demo.services.database.customer
|
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.models.CustomerExtended
|
||||||
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
|
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
package com.github.dannecron.demo.services.database.customer
|
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.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 com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
|
||||||
import java.time.OffsetDateTime
|
import org.springframework.stereotype.Service
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Service
|
||||||
class CustomerServiceImpl(
|
class CustomerServiceImpl(
|
||||||
private val customerRepository: CustomerRepository,
|
private val customerRepository: CustomerRepository,
|
||||||
private val cityRepository: CityRepository
|
private val cityRepository: CityRepository,
|
||||||
|
private val commonGenerator: CommonGenerator,
|
||||||
): CustomerService {
|
): CustomerService {
|
||||||
override fun findByGuid(guid: UUID): CustomerExtended? = customerRepository.findByGuid(guid)
|
override fun findByGuid(guid: UUID): CustomerExtended? = customerRepository.findByGuid(guid)
|
||||||
?.let {
|
?.let {
|
||||||
@@ -22,12 +25,12 @@ class CustomerServiceImpl(
|
|||||||
|
|
||||||
override fun create(name: String, cityGuid: UUID?): Customer = Customer(
|
override fun create(name: String, cityGuid: UUID?): Customer = Customer(
|
||||||
id = null,
|
id = null,
|
||||||
guid = UUID.randomUUID(),
|
guid = commonGenerator.generateUUID(),
|
||||||
name = name,
|
name = name,
|
||||||
cityId = cityGuid?.let {
|
cityId = cityGuid?.let {
|
||||||
cityRepository.findByGuid(it)?.id ?: throw CityNotFoundException()
|
cityRepository.findByGuid(it)?.id ?: throw CityNotFoundException()
|
||||||
},
|
},
|
||||||
createdAt = OffsetDateTime.now(),
|
createdAt = commonGenerator.generateCurrentTime(),
|
||||||
updatedAt = null,
|
updatedAt = null,
|
||||||
).let {
|
).let {
|
||||||
customerRepository.save(it)
|
customerRepository.save(it)
|
||||||
|
|||||||
@@ -1,55 +1,65 @@
|
|||||||
package com.github.dannecron.demo.services.database.order
|
package com.github.dannecron.demo.services.database.order
|
||||||
|
|
||||||
import com.github.dannecron.demo.models.Customer
|
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
||||||
import com.github.dannecron.demo.models.Product
|
import com.github.dannecron.demo.db.entity.Customer
|
||||||
import com.github.dannecron.demo.models.order.Order
|
import com.github.dannecron.demo.db.entity.Product
|
||||||
import com.github.dannecron.demo.models.order.OrderProduct
|
import com.github.dannecron.demo.db.entity.order.Order
|
||||||
import com.github.dannecron.demo.models.order.OrderWithProducts
|
import com.github.dannecron.demo.db.entity.order.OrderProduct
|
||||||
import com.github.dannecron.demo.providers.OrderProductRepository
|
import com.github.dannecron.demo.db.repository.OrderProductRepository
|
||||||
import com.github.dannecron.demo.providers.OrderRepository
|
import com.github.dannecron.demo.db.repository.OrderRepository
|
||||||
import com.github.dannecron.demo.providers.ProductRepository
|
import com.github.dannecron.demo.db.repository.ProductRepository
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import com.github.dannecron.demo.models.OrderWithProducts
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import java.time.OffsetDateTime
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class OrderServiceImpl(
|
class OrderServiceImpl(
|
||||||
@Autowired private val orderRepository: OrderRepository,
|
private val orderRepository: OrderRepository,
|
||||||
@Autowired private val orderProductRepository: OrderProductRepository,
|
private val orderProductRepository: OrderProductRepository,
|
||||||
@Autowired private val productRepository: ProductRepository,
|
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(
|
.let { orders -> orders.map { order -> OrderWithProducts(
|
||||||
order = order,
|
order = order,
|
||||||
products = orderProductRepository.findByOrderId(orderId = order.id!!)
|
products = findProductsByOrderId(order.id!!),
|
||||||
.map { orderProduct -> orderProduct.productId }
|
|
||||||
.let { productIds -> productRepository.findAllById(productIds).toList() }
|
|
||||||
) } }
|
) } }
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun createOrder(customer: Customer, products: Set<Product>): Order {
|
fun createOrder(customer: Customer, products: Set<Product>): Order {
|
||||||
val order = Order(
|
val order = Order(
|
||||||
id = null,
|
id = null,
|
||||||
guid = UUID.randomUUID(),
|
guid = commonGenerator.generateUUID(),
|
||||||
customerId = customer.id!!,
|
customerId = customer.id!!,
|
||||||
deliveredAt = null,
|
deliveredAt = null,
|
||||||
createdAt = OffsetDateTime.now(),
|
createdAt = commonGenerator.generateCurrentTime(),
|
||||||
updatedAt = null,
|
updatedAt = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
return orderRepository.save(order)
|
return orderRepository.save(order)
|
||||||
.also {
|
.also { saveProductsForNewOrder(it, products.toList()) }
|
||||||
savedOrder -> products.toList()
|
|
||||||
.map { product -> OrderProduct(
|
|
||||||
guid = UUID.randomUUID(),
|
|
||||||
orderId = savedOrder.id!!,
|
|
||||||
productId = product.id!!,
|
|
||||||
createdAt = OffsetDateTime.now(),
|
|
||||||
updatedAt = null
|
|
||||||
) }
|
|
||||||
.also { orderProductRepository.saveAll(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.github.dannecron.demo.services.database.product
|
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.AlreadyDeletedException
|
||||||
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
|
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
|
||||||
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
|
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
interface ProductService {
|
interface ProductService {
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
package com.github.dannecron.demo.services.database.product
|
package com.github.dannecron.demo.services.database.product
|
||||||
|
|
||||||
import com.github.dannecron.demo.models.Product
|
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
||||||
import com.github.dannecron.demo.providers.ProductRepository
|
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.AlreadyDeletedException
|
||||||
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
|
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
|
||||||
import com.github.dannecron.demo.services.kafka.Producer
|
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 com.github.dannecron.demo.utils.LoggerDelegate
|
||||||
import net.logstash.logback.marker.Markers
|
import net.logstash.logback.marker.Markers
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import java.time.OffsetDateTime
|
import org.springframework.stereotype.Service
|
||||||
import java.util.*
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Service
|
||||||
class ProductServiceImpl(
|
class ProductServiceImpl(
|
||||||
private val defaultSyncTopic: String,
|
|
||||||
private val productRepository: ProductRepository,
|
private val productRepository: ProductRepository,
|
||||||
private val producer: Producer,
|
private val producer: Producer,
|
||||||
|
private val commonGenerator: CommonGenerator,
|
||||||
): ProductService {
|
): ProductService {
|
||||||
private val logger by LoggerDelegate()
|
private val logger by LoggerDelegate()
|
||||||
|
|
||||||
@@ -32,11 +37,11 @@ class ProductServiceImpl(
|
|||||||
override fun create(name: String, price: Long, description: String?): Product {
|
override fun create(name: String, price: Long, description: String?): Product {
|
||||||
val product = Product(
|
val product = Product(
|
||||||
id = null,
|
id = null,
|
||||||
guid = UUID.randomUUID(),
|
guid = commonGenerator.generateUUID(),
|
||||||
name = name,
|
name = name,
|
||||||
description = description,
|
description = description,
|
||||||
price = price,
|
price = price,
|
||||||
createdAt = OffsetDateTime.now(),
|
createdAt = commonGenerator.generateCurrentTime(),
|
||||||
updatedAt = null,
|
updatedAt = null,
|
||||||
deletedAt = null,
|
deletedAt = null,
|
||||||
)
|
)
|
||||||
@@ -52,7 +57,7 @@ class ProductServiceImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val deletedProduct = product.copy(
|
val deletedProduct = product.copy(
|
||||||
deletedAt = OffsetDateTime.now(),
|
deletedAt = commonGenerator.generateCurrentTime(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return productRepository.save(deletedProduct)
|
return productRepository.save(deletedProduct)
|
||||||
@@ -61,7 +66,17 @@ class ProductServiceImpl(
|
|||||||
override fun syncToKafka(guid: UUID, topic: String?) {
|
override fun syncToKafka(guid: UUID, topic: String?) {
|
||||||
val product = findByGuid(guid) ?: throw ProductNotFoundException()
|
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),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package com.github.dannecron.demo.services.validation.exceptions
|
|
||||||
|
|
||||||
class SchemaNotFoundException: RuntimeException()
|
|
||||||
@@ -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() }
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,83 +1,147 @@
|
|||||||
package com.github.dannecron.demo.services.database.order
|
package com.github.dannecron.demo.services.database.order
|
||||||
|
|
||||||
import com.github.dannecron.demo.BaseDbTest
|
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
||||||
import com.github.dannecron.demo.models.Customer
|
import com.github.dannecron.demo.db.entity.Customer
|
||||||
import com.github.dannecron.demo.models.Product
|
import com.github.dannecron.demo.db.entity.Product
|
||||||
import com.github.dannecron.demo.models.order.Order
|
import com.github.dannecron.demo.db.entity.order.Order
|
||||||
import com.github.dannecron.demo.providers.CustomerRepository
|
import com.github.dannecron.demo.db.entity.order.OrderProduct
|
||||||
import com.github.dannecron.demo.providers.OrderRepository
|
import com.github.dannecron.demo.db.repository.OrderProductRepository
|
||||||
import com.github.dannecron.demo.providers.ProductRepository
|
import com.github.dannecron.demo.db.repository.OrderRepository
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import com.github.dannecron.demo.db.repository.ProductRepository
|
||||||
import org.springframework.test.context.ContextConfiguration
|
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.time.OffsetDateTime
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNotNull
|
|
||||||
|
|
||||||
@ContextConfiguration(classes = [OrderServiceImpl::class])
|
class OrderServiceImplTest {
|
||||||
class OrderServiceImplTest: BaseDbTest() {
|
private val mockGuid = UUID.randomUUID()
|
||||||
@Autowired
|
private val mockCurrentTime = OffsetDateTime.now()
|
||||||
private lateinit var orderRepository: OrderRepository
|
|
||||||
@Autowired
|
|
||||||
private lateinit var productRepository: ProductRepository
|
|
||||||
@Autowired
|
|
||||||
private lateinit var customerRepository: CustomerRepository
|
|
||||||
@Autowired
|
|
||||||
private lateinit var orderServiceImpl: OrderServiceImpl
|
|
||||||
|
|
||||||
@Test
|
private val commonGenerator: CommonGenerator = mock {
|
||||||
fun createAndFind_success() {
|
on { generateUUID() } doReturn mockGuid
|
||||||
var productOneId: Long? = null
|
on { generateCurrentTime() } doReturn mockCurrentTime
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val orderRepository: OrderRepository = mock()
|
||||||
|
private val productRepository: ProductRepository = mock()
|
||||||
|
private val orderProductRepository: OrderProductRepository = mock()
|
||||||
|
|
||||||
} finally {
|
private val orderServiceImpl = OrderServiceImpl(
|
||||||
order?.id?.also { orderRepository.deleteById(it) }
|
orderRepository = orderRepository,
|
||||||
customerId?.also { customerRepository.deleteById(it) }
|
orderProductRepository = orderProductRepository,
|
||||||
productOneId?.also { productRepository.deleteById(it) }
|
productRepository = productRepository,
|
||||||
productTwoId?.also { productRepository.deleteById(it) }
|
commonGenerator = commonGenerator,
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
private fun makeProduct(price: Long = 10000L): Product = Product(
|
private val now = OffsetDateTime.now()
|
||||||
id = null,
|
|
||||||
|
private val customerId = 123L
|
||||||
|
private val customer = Customer(
|
||||||
|
id = customerId,
|
||||||
guid = UUID.randomUUID(),
|
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,
|
description = null,
|
||||||
price = price,
|
price = 10000L,
|
||||||
createdAt = OffsetDateTime.now(),
|
createdAt = now.minusMonths(1),
|
||||||
updatedAt = null,
|
updatedAt = null,
|
||||||
deletedAt = null,
|
deletedAt = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun makeCustomer(): Customer = Customer(
|
private val orderProduct = OrderProduct(
|
||||||
id = null,
|
|
||||||
guid = UUID.randomUUID(),
|
guid = UUID.randomUUID(),
|
||||||
name = "client",
|
orderId = orderOneId,
|
||||||
cityId = null,
|
productId = productId,
|
||||||
createdAt = OffsetDateTime.now(),
|
createdAt = now.minusDays(1),
|
||||||
updatedAt = null,
|
updatedAt = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun findByCustomerId() {
|
||||||
|
whenever(orderRepository.findByCustomerId(any())).thenReturn(listOf(orderOne, orderTwo))
|
||||||
|
whenever(orderProductRepository.findByOrderId(any()))
|
||||||
|
.thenReturn(listOf(orderProduct))
|
||||||
|
.thenReturn(emptyList())
|
||||||
|
whenever(productRepository.findAllById(any())).thenReturn(listOf(product))
|
||||||
|
|
||||||
|
val expectedResult = listOf(
|
||||||
|
OrderWithProducts(
|
||||||
|
order = orderOne,
|
||||||
|
products = listOf(product),
|
||||||
|
),
|
||||||
|
OrderWithProducts(
|
||||||
|
order = orderTwo,
|
||||||
|
products = emptyList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = orderServiceImpl.findByCustomerId(customerId)
|
||||||
|
assertEquals(expectedResult, result)
|
||||||
|
|
||||||
|
verify(orderRepository, times(1)).findByCustomerId(customerId)
|
||||||
|
verify(orderProductRepository, times(1)).findByOrderId(orderOneId)
|
||||||
|
verify(orderProductRepository, times(1)).findByOrderId(orderTwoId)
|
||||||
|
verify(productRepository, times(1)).findAllById(listOf(productId))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun create() {
|
||||||
|
val newOrder = orderTwo.copy(
|
||||||
|
guid = mockGuid,
|
||||||
|
createdAt = mockCurrentTime,
|
||||||
|
)
|
||||||
|
val newOrderProduct = orderProduct.copy(
|
||||||
|
guid = mockGuid,
|
||||||
|
createdAt = mockCurrentTime,
|
||||||
|
orderId = orderTwoId,
|
||||||
|
)
|
||||||
|
|
||||||
|
whenever(orderRepository.save(any<Order>())).thenReturn(newOrder)
|
||||||
|
whenever(orderProductRepository.saveAll(any<List<OrderProduct>>())).thenReturn(listOf(newOrderProduct))
|
||||||
|
|
||||||
|
val result = orderServiceImpl.createOrder(
|
||||||
|
customer = customer,
|
||||||
|
products = setOf(product),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(newOrder, result)
|
||||||
|
|
||||||
|
verify(orderRepository, times(1)).save(newOrder.copy(id = null))
|
||||||
|
verify(orderProductRepository, times(1)).saveAll(listOf(newOrderProduct))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +1,46 @@
|
|||||||
package com.github.dannecron.demo.services.database.product
|
package com.github.dannecron.demo.services.database.product
|
||||||
|
|
||||||
import com.github.dannecron.demo.BaseUnitTest
|
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
||||||
import com.github.dannecron.demo.models.Product
|
import com.github.dannecron.demo.db.entity.Product
|
||||||
import com.github.dannecron.demo.providers.ProductRepository
|
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.database.exceptions.ProductNotFoundException
|
||||||
import com.github.dannecron.demo.services.kafka.Producer
|
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.jupiter.api.assertThrows
|
||||||
import org.junit.runner.RunWith
|
import org.mockito.kotlin.any
|
||||||
import org.mockito.kotlin.*
|
import org.mockito.kotlin.doReturn
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.mockito.kotlin.mock
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.mockito.kotlin.never
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean
|
import org.mockito.kotlin.times
|
||||||
import org.springframework.test.context.junit4.SpringRunner
|
import org.mockito.kotlin.verify
|
||||||
|
import org.mockito.kotlin.verifyNoInteractions
|
||||||
|
import org.mockito.kotlin.whenever
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.util.*
|
import java.time.format.DateTimeFormatter
|
||||||
import kotlin.test.BeforeTest
|
import java.util.UUID
|
||||||
import kotlin.test.Test
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
@RunWith(SpringRunner::class)
|
class ProductServiceImplTest {
|
||||||
@SpringBootTest
|
private val mockGuid = UUID.randomUUID()
|
||||||
class ProductServiceImplTest: BaseUnitTest() {
|
private val mockCurrentTime = OffsetDateTime.now()
|
||||||
private val defaultTopic = "some-default-topic"
|
|
||||||
private lateinit var productService: ProductServiceImpl
|
|
||||||
|
|
||||||
@MockBean
|
private val productRepository: ProductRepository = mock()
|
||||||
@Qualifier("producer")
|
private val producer: Producer = mock()
|
||||||
private lateinit var producer: Producer
|
private val commonGenerator: CommonGenerator = mock {
|
||||||
@MockBean
|
on { generateUUID() } doReturn mockGuid
|
||||||
private lateinit var productRepository: ProductRepository
|
on { generateCurrentTime() } doReturn mockCurrentTime
|
||||||
|
|
||||||
@BeforeTest
|
|
||||||
fun setUp() {
|
|
||||||
productService = ProductServiceImpl(
|
|
||||||
defaultSyncTopic = defaultTopic,
|
|
||||||
productRepository = productRepository,
|
|
||||||
producer = producer,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private val productService = ProductServiceImpl(
|
||||||
fun syncToKafka_success() {
|
productRepository = productRepository,
|
||||||
val guid = UUID.randomUUID()
|
producer = producer,
|
||||||
val product = Product(
|
commonGenerator = commonGenerator,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val guid = UUID.randomUUID()
|
||||||
|
private val product = Product(
|
||||||
id = 123,
|
id = 123,
|
||||||
guid = guid,
|
guid = guid,
|
||||||
name = "name",
|
name = "name",
|
||||||
@@ -50,50 +48,102 @@ class ProductServiceImplTest: BaseUnitTest() {
|
|||||||
price = 10050,
|
price = 10050,
|
||||||
createdAt = OffsetDateTime.now().minusDays(1),
|
createdAt = OffsetDateTime.now().minusDays(1),
|
||||||
updatedAt = OffsetDateTime.now().minusHours(2),
|
updatedAt = OffsetDateTime.now().minusHours(2),
|
||||||
deletedAt = OffsetDateTime.now(),
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
whenever(productRepository.findByGuid(eq(guid))) doReturn product
|
@Test
|
||||||
whenever(producer.produceProductInfo(defaultTopic, product)) doAnswer {}
|
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",
|
||||||
|
price = 10050,
|
||||||
|
description = "description",
|
||||||
|
)
|
||||||
|
assertEquals(expectedCreatedProduct, result)
|
||||||
|
|
||||||
|
verify(productRepository, times(1)).save(expectedProductForCreation)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `delete - success`() {
|
||||||
|
val deletedProduct = product.copy(
|
||||||
|
deletedAt = mockCurrentTime,
|
||||||
|
)
|
||||||
|
whenever(productRepository.findByGuid(any())).thenReturn(product)
|
||||||
|
whenever(productRepository.save<Product>(any())).thenReturn(deletedProduct)
|
||||||
|
|
||||||
|
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.delete(guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(productRepository, times(1)).findByGuid(guid)
|
||||||
|
verify(productRepository, never()).save(any())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `syncToKafka - success`() {
|
||||||
|
whenever(productRepository.findByGuid(any())) doReturn product
|
||||||
|
|
||||||
|
productService.syncToKafka(guid, null)
|
||||||
|
|
||||||
|
verify(productRepository, times(1)).findByGuid(guid)
|
||||||
|
verify(producer, times(1)).produceProductSync(kafkaProductDto)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `syncToKafka - not found`() {
|
||||||
|
whenever(productRepository.findByGuid(any())) doReturn null
|
||||||
|
|
||||||
|
assertThrows<ProductNotFoundException> {
|
||||||
productService.syncToKafka(guid, null)
|
productService.syncToKafka(guid, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
verify(productRepository, times(1)).findByGuid(guid)
|
||||||
fun syncToKafka_notFound() {
|
|
||||||
val specificTopic = "specificNotice"
|
|
||||||
val guid = UUID.randomUUID()
|
|
||||||
|
|
||||||
whenever(productRepository.findByGuid(eq(guid))) doReturn null
|
|
||||||
|
|
||||||
assertThrows<ProductNotFoundException> {
|
|
||||||
productService.syncToKafka(guid, specificTopic)
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyNoInteractions(producer)
|
verifyNoInteractions(producer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun syncToKafka_invalidArgumentException() {
|
|
||||||
val specificTopic = "specificNotice"
|
|
||||||
val guid = UUID.randomUUID()
|
|
||||||
|
|
||||||
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(),
|
|
||||||
)
|
|
||||||
|
|
||||||
whenever(productRepository.findByGuid(eq(guid))) doReturn product
|
|
||||||
whenever(producer.produceProductInfo(specificTopic, product)) doThrow InvalidArgumentException("some error")
|
|
||||||
|
|
||||||
assertThrows< InvalidArgumentException> {
|
|
||||||
productService.syncToKafka(guid, specificTopic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user