From d7c051746df3ce10a7c224bd578c000cf767e1b7 Mon Sep 17 00:00:00 2001 From: Savosin Denis Date: Fri, 28 Mar 2025 14:38:57 +0700 Subject: [PATCH] move repositories to separate db package replace jacoco with kover --- build.gradle.kts | 88 ++++++++++++------- db/build.gradle.kts | 16 ++++ .../github/dannecron/demo/db/entity/City.kt | 31 +++++++ .../dannecron/demo/db/entity/Customer.kt | 27 ++++++ .../dannecron/demo/db/entity/Product.kt | 42 +++++++++ .../dannecron/demo/db/entity/order/Order.kt | 31 +++++++ .../demo/db/entity/order/OrderProduct.kt | 41 +++++++++ .../demo/db/repository/CityRepository.kt | 17 ++++ .../demo/db/repository/CustomerRepository.kt | 11 +++ .../db/repository/OrderProductRepository.kt | 11 +++ .../demo/db/repository/OrderRepository.kt | 10 +++ .../demo/db/repository/ProductRepository.kt | 17 ++++ .../OffsetDateTimeSerialization.kt | 19 ++++ .../db/serialialization/UuidSerialization.kt | 18 ++++ .../data/V1.1__insert_product_data.sql | 0 .../migration/data/V4.1__insert_city_data.sql | 0 .../structure/V1__create_product_table.sql | 0 .../V2__add_deteled_at_to_product_table.sql | 0 .../V3__add_unique_index_to_product_guid.sql | 0 .../structure/V4__create_city_table.sql | 0 .../structure/V5__create_customer_table.sql | 0 .../structure/V6__create_order_table.sql | 0 .../V7__create_order_product_table.sql | 0 .../github/dannecron/demo/db/BaseDbTest.kt | 14 +++ .../demo/db/repository/CityRepositoryTest.kt | 49 +++++++++++ .../db/repository/CustomerRepositoryTest.kt | 39 ++++++++ .../repository/OrderProductRepositoryTest.kt | 43 +++++++++ .../demo/db/repository/OrderRepositoryTest.kt | 43 +++++++++ .../db/repository/ProductRepositoryTest.kt | 41 +++++++++ db/src/test/resources/application-db.yml | 10 +++ db/src/test/resources/sql/insert_city.sql | 3 + db/src/test/resources/sql/insert_customer.sql | 3 + db/src/test/resources/sql/insert_order.sql | 3 + .../resources/sql/insert_order_product.sql | 3 + db/src/test/resources/sql/insert_product.sql | 3 + gradle/libs.versions.toml | 3 +- settings.gradle.kts | 1 + 37 files changed, 603 insertions(+), 34 deletions(-) create mode 100644 db/build.gradle.kts create mode 100644 db/src/main/kotlin/com/github/dannecron/demo/db/entity/City.kt create mode 100644 db/src/main/kotlin/com/github/dannecron/demo/db/entity/Customer.kt create mode 100644 db/src/main/kotlin/com/github/dannecron/demo/db/entity/Product.kt create mode 100644 db/src/main/kotlin/com/github/dannecron/demo/db/entity/order/Order.kt create mode 100644 db/src/main/kotlin/com/github/dannecron/demo/db/entity/order/OrderProduct.kt create mode 100644 db/src/main/kotlin/com/github/dannecron/demo/db/repository/CityRepository.kt create mode 100644 db/src/main/kotlin/com/github/dannecron/demo/db/repository/CustomerRepository.kt create mode 100644 db/src/main/kotlin/com/github/dannecron/demo/db/repository/OrderProductRepository.kt create mode 100644 db/src/main/kotlin/com/github/dannecron/demo/db/repository/OrderRepository.kt create mode 100644 db/src/main/kotlin/com/github/dannecron/demo/db/repository/ProductRepository.kt create mode 100644 db/src/main/kotlin/com/github/dannecron/demo/db/serialialization/OffsetDateTimeSerialization.kt create mode 100644 db/src/main/kotlin/com/github/dannecron/demo/db/serialialization/UuidSerialization.kt rename {src/main/resources/db => db/src/main/resources}/migration/data/V1.1__insert_product_data.sql (100%) rename {src/main/resources/db => db/src/main/resources}/migration/data/V4.1__insert_city_data.sql (100%) rename {src/main/resources/db => db/src/main/resources}/migration/structure/V1__create_product_table.sql (100%) rename {src/main/resources/db => db/src/main/resources}/migration/structure/V2__add_deteled_at_to_product_table.sql (100%) rename {src/main/resources/db => db/src/main/resources}/migration/structure/V3__add_unique_index_to_product_guid.sql (100%) rename {src/main/resources/db => db/src/main/resources}/migration/structure/V4__create_city_table.sql (100%) rename {src/main/resources/db => db/src/main/resources}/migration/structure/V5__create_customer_table.sql (100%) rename {src/main/resources/db => db/src/main/resources}/migration/structure/V6__create_order_table.sql (100%) rename {src/main/resources/db => db/src/main/resources}/migration/structure/V7__create_order_product_table.sql (100%) create mode 100644 db/src/test/kotlin/com/github/dannecron/demo/db/BaseDbTest.kt create mode 100644 db/src/test/kotlin/com/github/dannecron/demo/db/repository/CityRepositoryTest.kt create mode 100644 db/src/test/kotlin/com/github/dannecron/demo/db/repository/CustomerRepositoryTest.kt create mode 100644 db/src/test/kotlin/com/github/dannecron/demo/db/repository/OrderProductRepositoryTest.kt create mode 100644 db/src/test/kotlin/com/github/dannecron/demo/db/repository/OrderRepositoryTest.kt create mode 100644 db/src/test/kotlin/com/github/dannecron/demo/db/repository/ProductRepositoryTest.kt create mode 100644 db/src/test/resources/application-db.yml create mode 100644 db/src/test/resources/sql/insert_city.sql create mode 100644 db/src/test/resources/sql/insert_customer.sql create mode 100644 db/src/test/resources/sql/insert_order.sql create mode 100644 db/src/test/resources/sql/insert_order_product.sql create mode 100644 db/src/test/resources/sql/insert_product.sql diff --git a/build.gradle.kts b/build.gradle.kts index c628bc9..d1fb504 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,40 +1,85 @@ plugins { + idea + alias(libs.plugins.kotlin.kover) alias(libs.plugins.kotlin.jpa) alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.spring) alias(libs.plugins.spring.boot) alias(libs.plugins.spring.dependencyManagement) - - jacoco } group = "com.github.dannecron.demo" version = "single-version" -java { - sourceCompatibility = JavaVersion.VERSION_17 +allprojects { + apply { + plugin(rootProject.libs.plugins.kotlin.jvm.get().pluginId) + plugin(rootProject.libs.plugins.kotlin.serialization.get().pluginId) + plugin(rootProject.libs.plugins.kotlin.kover.get().pluginId) + + plugin("java") + } + + plugins.withId("org.jetbrains.kotlinx.kover") { + tasks.named("koverXmlReport") { + dependsOn(tasks.test) + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_17 + } + kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1) + } + } + + repositories { + mavenCentral() + } + + dependencies { + implementation(rootProject.libs.kotlin.reflect) + implementation(rootProject.libs.kotlinx.serialization.json) + implementation(rootProject.libs.spring.aspects) + + testImplementation(rootProject.libs.kotlin.test.junit) + testImplementation(rootProject.libs.mockito.kotlin) + testImplementation(rootProject.libs.spring.boot.starter.test) + + kover(project(":db")) + } + + tasks.test { + useJUnitPlatform() + finalizedBy("koverXmlReport") + } } -repositories { - mavenCentral() +subprojects { + apply { + plugin(rootProject.libs.plugins.kotlin.spring.get().pluginId) + plugin(rootProject.libs.plugins.spring.boot.get().pluginId) + plugin(rootProject.libs.plugins.spring.dependencyManagement.get().pluginId) + } } dependencies { + implementation(project(":db")) + runtimeOnly(libs.micrometer.registry.prometheus) implementation(libs.bundles.tracing) - implementation(libs.flyway.core) implementation(libs.jackson.datatype.jsr) implementation(libs.jackson.module.kotlin) implementation(libs.json.schema.validator) - implementation(libs.kotlin.reflect) - implementation(libs.kotlinx.serialization.json) implementation(libs.ktor.client.cio) implementation(libs.ktor.client.core) implementation(libs.logback.encoder) implementation(libs.postgres) - implementation(libs.spring.aspects) implementation(libs.spring.boot.starter.actuator) implementation(libs.spring.boot.starter.jdbc) implementation(libs.spring.boot.starter.mustache) @@ -43,33 +88,10 @@ dependencies { implementation(libs.spring.doc.openapi.starter) implementation(libs.spring.kafka) - testImplementation(libs.kotlin.test.junit) - testImplementation(libs.mockito.kotlin) - testImplementation(libs.spring.boot.starter.test) testImplementation(libs.spring.kafka.test) testImplementation(libs.testcontainers) testImplementation(libs.testcontainers.junit.jupiter) - testImplementation(libs.testcontainers.postgresql) testImplementation(libs.ktor.client.mock) developmentOnly(libs.spring.boot.devtools) } - -kotlin { - compilerOptions { - freeCompilerArgs.addAll("-Xjsr305=strict") - apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1) - } -} - -tasks.withType { - useJUnitPlatform() -} - -tasks.test { - finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run -} - -tasks.jacocoTestReport { - dependsOn(tasks.test) // tests are required to run before generating the report -} diff --git a/db/build.gradle.kts b/db/build.gradle.kts new file mode 100644 index 0000000..c994b6b --- /dev/null +++ b/db/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.kotlin.jpa) +} + +group = "com.github.dannecron.demo" +version = "single-version" + +dependencies { + implementation(rootProject.libs.flyway.core) + implementation(rootProject.libs.postgres) + implementation(rootProject.libs.spring.boot.starter.jdbc) + + testImplementation(libs.testcontainers) + testImplementation(libs.testcontainers.junit.jupiter) + testImplementation(libs.testcontainers.postgresql) +} diff --git a/db/src/main/kotlin/com/github/dannecron/demo/db/entity/City.kt b/db/src/main/kotlin/com/github/dannecron/demo/db/entity/City.kt new file mode 100644 index 0000000..543f8b2 --- /dev/null +++ b/db/src/main/kotlin/com/github/dannecron/demo/db/entity/City.kt @@ -0,0 +1,31 @@ +package com.github.dannecron.demo.db.entity + +import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization +import com.github.dannecron.demo.db.serialialization.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.UUID + +@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/db/src/main/kotlin/com/github/dannecron/demo/db/entity/Customer.kt b/db/src/main/kotlin/com/github/dannecron/demo/db/entity/Customer.kt new file mode 100644 index 0000000..ddbde0f --- /dev/null +++ b/db/src/main/kotlin/com/github/dannecron/demo/db/entity/Customer.kt @@ -0,0 +1,27 @@ +package com.github.dannecron.demo.db.entity + +import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization +import com.github.dannecron.demo.db.serialialization.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.UUID + +@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/db/src/main/kotlin/com/github/dannecron/demo/db/entity/Product.kt b/db/src/main/kotlin/com/github/dannecron/demo/db/entity/Product.kt new file mode 100644 index 0000000..b2d7d26 --- /dev/null +++ b/db/src/main/kotlin/com/github/dannecron/demo/db/entity/Product.kt @@ -0,0 +1,42 @@ +package com.github.dannecron.demo.db.entity + +import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization +import com.github.dannecron.demo.db.serialialization.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.UUID +import kotlin.math.pow +import kotlin.math.roundToInt + +@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 + + private fun Double.roundTo(numFractionDigits: Int): Double { + val factor = 10.0.pow(numFractionDigits.toDouble()) + return (this * factor).roundToInt() / factor + } +} diff --git a/db/src/main/kotlin/com/github/dannecron/demo/db/entity/order/Order.kt b/db/src/main/kotlin/com/github/dannecron/demo/db/entity/order/Order.kt new file mode 100644 index 0000000..984a3b2 --- /dev/null +++ b/db/src/main/kotlin/com/github/dannecron/demo/db/entity/order/Order.kt @@ -0,0 +1,31 @@ +package com.github.dannecron.demo.db.entity.order + +import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization +import com.github.dannecron.demo.db.serialialization.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.UUID + +@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/db/src/main/kotlin/com/github/dannecron/demo/db/entity/order/OrderProduct.kt b/db/src/main/kotlin/com/github/dannecron/demo/db/entity/order/OrderProduct.kt new file mode 100644 index 0000000..a6b42ac --- /dev/null +++ b/db/src/main/kotlin/com/github/dannecron/demo/db/entity/order/OrderProduct.kt @@ -0,0 +1,41 @@ +package com.github.dannecron.demo.db.entity.order + +import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization +import com.github.dannecron.demo.db.serialialization.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.UUID + +@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/db/src/main/kotlin/com/github/dannecron/demo/db/repository/CityRepository.kt b/db/src/main/kotlin/com/github/dannecron/demo/db/repository/CityRepository.kt new file mode 100644 index 0000000..9ec47ad --- /dev/null +++ b/db/src/main/kotlin/com/github/dannecron/demo/db/repository/CityRepository.kt @@ -0,0 +1,17 @@ +package com.github.dannecron.demo.db.repository + +import com.github.dannecron.demo.db.entity.City +import org.springframework.data.jdbc.repository.query.Query +import org.springframework.data.repository.CrudRepository +import org.springframework.data.repository.query.Param +import org.springframework.stereotype.Repository +import java.time.OffsetDateTime +import java.util.UUID + +@Repository +interface CityRepository: CrudRepository { + fun findByGuid(guid: UUID): City? + + @Query(value = "UPDATE city SET deleted_at = :deletedAt WHERE guid = :guid RETURNING *") + fun softDelete(@Param("guid") guid: UUID, @Param("deletedAt") deletedAt: OffsetDateTime): City? +} diff --git a/db/src/main/kotlin/com/github/dannecron/demo/db/repository/CustomerRepository.kt b/db/src/main/kotlin/com/github/dannecron/demo/db/repository/CustomerRepository.kt new file mode 100644 index 0000000..065eca0 --- /dev/null +++ b/db/src/main/kotlin/com/github/dannecron/demo/db/repository/CustomerRepository.kt @@ -0,0 +1,11 @@ +package com.github.dannecron.demo.db.repository + +import com.github.dannecron.demo.db.entity.Customer +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface CustomerRepository: CrudRepository { + fun findByGuid(guid: UUID): Customer? +} diff --git a/db/src/main/kotlin/com/github/dannecron/demo/db/repository/OrderProductRepository.kt b/db/src/main/kotlin/com/github/dannecron/demo/db/repository/OrderProductRepository.kt new file mode 100644 index 0000000..90a6c85 --- /dev/null +++ b/db/src/main/kotlin/com/github/dannecron/demo/db/repository/OrderProductRepository.kt @@ -0,0 +1,11 @@ +package com.github.dannecron.demo.db.repository + +import com.github.dannecron.demo.db.entity.order.OrderProduct +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository +import java.util.UUID + +@Repository +interface OrderProductRepository: CrudRepository { + fun findByOrderId(orderId: Long): List +} diff --git a/db/src/main/kotlin/com/github/dannecron/demo/db/repository/OrderRepository.kt b/db/src/main/kotlin/com/github/dannecron/demo/db/repository/OrderRepository.kt new file mode 100644 index 0000000..05a8079 --- /dev/null +++ b/db/src/main/kotlin/com/github/dannecron/demo/db/repository/OrderRepository.kt @@ -0,0 +1,10 @@ +package com.github.dannecron.demo.db.repository + +import com.github.dannecron.demo.db.entity.order.Order +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository + +@Repository +interface OrderRepository: CrudRepository { + fun findByCustomerId(customerId: Long): List +} diff --git a/db/src/main/kotlin/com/github/dannecron/demo/db/repository/ProductRepository.kt b/db/src/main/kotlin/com/github/dannecron/demo/db/repository/ProductRepository.kt new file mode 100644 index 0000000..d7407f7 --- /dev/null +++ b/db/src/main/kotlin/com/github/dannecron/demo/db/repository/ProductRepository.kt @@ -0,0 +1,17 @@ +package com.github.dannecron.demo.db.repository + +import com.github.dannecron.demo.db.entity.Product +import org.springframework.data.jdbc.repository.query.Query +import org.springframework.data.repository.CrudRepository +import org.springframework.data.repository.PagingAndSortingRepository +import org.springframework.stereotype.Repository +import java.time.OffsetDateTime +import java.util.UUID + +@Repository +interface ProductRepository: CrudRepository, PagingAndSortingRepository { + fun findByGuid(guid: UUID): Product? + + @Query(value = "UPDATE Product SET deleted_at = :deletedAt WHERE guid = :guid RETURNING *") + fun softDelete(guid: UUID, deletedAt: OffsetDateTime): Product? +} diff --git a/db/src/main/kotlin/com/github/dannecron/demo/db/serialialization/OffsetDateTimeSerialization.kt b/db/src/main/kotlin/com/github/dannecron/demo/db/serialialization/OffsetDateTimeSerialization.kt new file mode 100644 index 0000000..56d3470 --- /dev/null +++ b/db/src/main/kotlin/com/github/dannecron/demo/db/serialialization/OffsetDateTimeSerialization.kt @@ -0,0 +1,19 @@ +package com.github.dannecron.demo.db.serialialization + +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/db/src/main/kotlin/com/github/dannecron/demo/db/serialialization/UuidSerialization.kt b/db/src/main/kotlin/com/github/dannecron/demo/db/serialialization/UuidSerialization.kt new file mode 100644 index 0000000..9e7fd2b --- /dev/null +++ b/db/src/main/kotlin/com/github/dannecron/demo/db/serialialization/UuidSerialization.kt @@ -0,0 +1,18 @@ +package com.github.dannecron.demo.db.serialialization + +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.UUID + +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/resources/db/migration/data/V1.1__insert_product_data.sql b/db/src/main/resources/migration/data/V1.1__insert_product_data.sql similarity index 100% rename from src/main/resources/db/migration/data/V1.1__insert_product_data.sql rename to db/src/main/resources/migration/data/V1.1__insert_product_data.sql diff --git a/src/main/resources/db/migration/data/V4.1__insert_city_data.sql b/db/src/main/resources/migration/data/V4.1__insert_city_data.sql similarity index 100% rename from src/main/resources/db/migration/data/V4.1__insert_city_data.sql rename to db/src/main/resources/migration/data/V4.1__insert_city_data.sql diff --git a/src/main/resources/db/migration/structure/V1__create_product_table.sql b/db/src/main/resources/migration/structure/V1__create_product_table.sql similarity index 100% rename from src/main/resources/db/migration/structure/V1__create_product_table.sql rename to db/src/main/resources/migration/structure/V1__create_product_table.sql diff --git a/src/main/resources/db/migration/structure/V2__add_deteled_at_to_product_table.sql b/db/src/main/resources/migration/structure/V2__add_deteled_at_to_product_table.sql similarity index 100% rename from src/main/resources/db/migration/structure/V2__add_deteled_at_to_product_table.sql rename to db/src/main/resources/migration/structure/V2__add_deteled_at_to_product_table.sql diff --git a/src/main/resources/db/migration/structure/V3__add_unique_index_to_product_guid.sql b/db/src/main/resources/migration/structure/V3__add_unique_index_to_product_guid.sql similarity index 100% rename from src/main/resources/db/migration/structure/V3__add_unique_index_to_product_guid.sql rename to db/src/main/resources/migration/structure/V3__add_unique_index_to_product_guid.sql diff --git a/src/main/resources/db/migration/structure/V4__create_city_table.sql b/db/src/main/resources/migration/structure/V4__create_city_table.sql similarity index 100% rename from src/main/resources/db/migration/structure/V4__create_city_table.sql rename to db/src/main/resources/migration/structure/V4__create_city_table.sql diff --git a/src/main/resources/db/migration/structure/V5__create_customer_table.sql b/db/src/main/resources/migration/structure/V5__create_customer_table.sql similarity index 100% rename from src/main/resources/db/migration/structure/V5__create_customer_table.sql rename to db/src/main/resources/migration/structure/V5__create_customer_table.sql diff --git a/src/main/resources/db/migration/structure/V6__create_order_table.sql b/db/src/main/resources/migration/structure/V6__create_order_table.sql similarity index 100% rename from src/main/resources/db/migration/structure/V6__create_order_table.sql rename to db/src/main/resources/migration/structure/V6__create_order_table.sql diff --git a/src/main/resources/db/migration/structure/V7__create_order_product_table.sql b/db/src/main/resources/migration/structure/V7__create_order_product_table.sql similarity index 100% rename from src/main/resources/db/migration/structure/V7__create_order_product_table.sql rename to db/src/main/resources/migration/structure/V7__create_order_product_table.sql diff --git a/db/src/test/kotlin/com/github/dannecron/demo/db/BaseDbTest.kt b/db/src/test/kotlin/com/github/dannecron/demo/db/BaseDbTest.kt new file mode 100644 index 0000000..ed2d340 --- /dev/null +++ b/db/src/test/kotlin/com/github/dannecron/demo/db/BaseDbTest.kt @@ -0,0 +1,14 @@ +package com.github.dannecron.demo.db + +import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +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 diff --git a/db/src/test/kotlin/com/github/dannecron/demo/db/repository/CityRepositoryTest.kt b/db/src/test/kotlin/com/github/dannecron/demo/db/repository/CityRepositoryTest.kt new file mode 100644 index 0000000..d9a9ba2 --- /dev/null +++ b/db/src/test/kotlin/com/github/dannecron/demo/db/repository/CityRepositoryTest.kt @@ -0,0 +1,49 @@ +package com.github.dannecron.demo.db.repository + +import com.github.dannecron.demo.db.BaseDbTest +import com.github.dannecron.demo.db.entity.City +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.jdbc.Sql +import java.time.OffsetDateTime +import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +@ContextConfiguration(classes = [CityRepository::class]) +class CityRepositoryTest : BaseDbTest() { + + @Autowired + private lateinit var cityRepository: CityRepository + + private val cityGuid = UUID.fromString("21a1a3a8-621a-40f7-b64f-7e118aa241b9") + private val city = City( + id = 1000, + guid = cityGuid, + name = "Tokyo", + createdAt = OffsetDateTime.parse("2025-01-01T12:10:05+00:00"), + updatedAt = null, + deletedAt = null, + ) + + @Test + @Sql(scripts = ["/sql/insert_city.sql"]) + fun findByGuid() { + val result = cityRepository.findByGuid(cityGuid) + assertEquals(city, result) + + val emptyResult = cityRepository.findByGuid(UUID.randomUUID()) + assertNull(emptyResult) + } + + @Test + @Sql(scripts = ["/sql/insert_city.sql"]) + fun softDelete() { + val deletedAt = OffsetDateTime.parse("2025-01-02T12:10:05+00:00") + val expectedCity = city.copy(deletedAt = deletedAt) + + val result = cityRepository.softDelete(cityGuid, deletedAt) + assertEquals(expectedCity, result) + } +} diff --git a/db/src/test/kotlin/com/github/dannecron/demo/db/repository/CustomerRepositoryTest.kt b/db/src/test/kotlin/com/github/dannecron/demo/db/repository/CustomerRepositoryTest.kt new file mode 100644 index 0000000..868b514 --- /dev/null +++ b/db/src/test/kotlin/com/github/dannecron/demo/db/repository/CustomerRepositoryTest.kt @@ -0,0 +1,39 @@ +package com.github.dannecron.demo.db.repository + +import com.github.dannecron.demo.db.BaseDbTest +import com.github.dannecron.demo.db.entity.Customer +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.jdbc.Sql +import java.time.OffsetDateTime +import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +@ContextConfiguration(classes = [CustomerRepository::class]) +class CustomerRepositoryTest : BaseDbTest() { + + @Autowired + private lateinit var customerRepository: CustomerRepository + + private val customerGuid = UUID.fromString("823c50de-4c81-49bd-a69a-2d52be42b728") + private val customer = Customer( + id = 1000, + guid = customerGuid, + name = "Customer", + cityId = 1000, + createdAt = OffsetDateTime.parse("2025-01-01T12:10:05+00:00"), + updatedAt = null, + ) + + @Test + @Sql(scripts = ["/sql/insert_city.sql", "/sql/insert_customer.sql"]) + fun findByGuid() { + val result = customerRepository.findByGuid(customerGuid) + assertEquals(customer, result) + + val emptyResult = customerRepository.findByGuid(UUID.randomUUID()) + assertNull(emptyResult) + } +} diff --git a/db/src/test/kotlin/com/github/dannecron/demo/db/repository/OrderProductRepositoryTest.kt b/db/src/test/kotlin/com/github/dannecron/demo/db/repository/OrderProductRepositoryTest.kt new file mode 100644 index 0000000..a6dbe0f --- /dev/null +++ b/db/src/test/kotlin/com/github/dannecron/demo/db/repository/OrderProductRepositoryTest.kt @@ -0,0 +1,43 @@ +package com.github.dannecron.demo.db.repository + +import com.github.dannecron.demo.db.BaseDbTest +import com.github.dannecron.demo.db.entity.order.OrderProduct +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.jdbc.Sql +import java.time.OffsetDateTime +import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertEquals + +@ContextConfiguration(classes = [OrderProductRepository::class]) +class OrderProductRepositoryTest : BaseDbTest() { + + @Autowired + private lateinit var orderProductRepository: OrderProductRepository + + private val orderId = 1000L + private val orderProduct = OrderProduct( + guid = UUID.fromString("930f54e2-c60d-448e-83b1-0d259ff2c2d3"), + orderId = orderId, + productId = 1000, + createdAt = OffsetDateTime.parse("2025-01-01T12:10:05+00:00"), + updatedAt = null, + ) + + @Test + @Sql( + scripts = [ + "/sql/insert_city.sql", + "/sql/insert_customer.sql", + "/sql/insert_order.sql", + "/sql/insert_product.sql", + "/sql/insert_order_product.sql", + ] + ) + fun findByOrderId() { + val result = orderProductRepository.findByOrderId(orderId) + assertEquals(1, result.size) + assertEquals(orderProduct, result[0]) + } +} diff --git a/db/src/test/kotlin/com/github/dannecron/demo/db/repository/OrderRepositoryTest.kt b/db/src/test/kotlin/com/github/dannecron/demo/db/repository/OrderRepositoryTest.kt new file mode 100644 index 0000000..569439d --- /dev/null +++ b/db/src/test/kotlin/com/github/dannecron/demo/db/repository/OrderRepositoryTest.kt @@ -0,0 +1,43 @@ +package com.github.dannecron.demo.db.repository + +import com.github.dannecron.demo.db.BaseDbTest +import com.github.dannecron.demo.db.entity.order.Order +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.jdbc.Sql +import java.time.OffsetDateTime +import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertEquals + +@ContextConfiguration(classes = [OrderRepository::class]) +class OrderRepositoryTest : BaseDbTest() { + + @Autowired + private lateinit var orderRepository: OrderRepository + + private val orderGuid = UUID.fromString("2c960a08-7187-4e91-9ef3-275c91b1342c") + private val customerId = 1000L + private val order = Order( + id = 1000, + guid = orderGuid, + customerId = customerId, + deliveredAt = null, + createdAt = OffsetDateTime.parse("2025-01-01T12:10:05+00:00"), + updatedAt = null, + ) + + @Test + @Sql( + scripts = [ + "/sql/insert_city.sql", + "/sql/insert_customer.sql", + "/sql/insert_order.sql", + ] + ) + fun findByGuid() { + val result = orderRepository.findByCustomerId(customerId) + assertEquals(1, result.size) + assertEquals(order, result[0]) + } +} diff --git a/db/src/test/kotlin/com/github/dannecron/demo/db/repository/ProductRepositoryTest.kt b/db/src/test/kotlin/com/github/dannecron/demo/db/repository/ProductRepositoryTest.kt new file mode 100644 index 0000000..c8af2c3 --- /dev/null +++ b/db/src/test/kotlin/com/github/dannecron/demo/db/repository/ProductRepositoryTest.kt @@ -0,0 +1,41 @@ +package com.github.dannecron.demo.db.repository + +import com.github.dannecron.demo.db.BaseDbTest +import com.github.dannecron.demo.db.entity.Product +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.jdbc.Sql +import java.time.OffsetDateTime +import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +@ContextConfiguration(classes = [ProductRepository::class]) +class ProductRepositoryTest : BaseDbTest() { + + @Autowired + private lateinit var productRepository: ProductRepository + + private val productGuid = UUID.fromString("1fb5c7e4-8ce2-43b8-8ca7-1089b04959b9") + private val product = Product( + id = 1000, + guid = productGuid, + name = "product", + description = "description", + price = 10000, + createdAt = OffsetDateTime.parse("2025-01-01T12:10:05+00:00"), + updatedAt = null, + deletedAt = null, + ) + + @Test + @Sql(scripts = ["/sql/insert_product.sql"]) + fun findByGuid() { + val result = productRepository.findByGuid(productGuid) + assertEquals(product, result) + + val emptyResult = productRepository.findByGuid(UUID.randomUUID()) + assertNull(emptyResult) + } +} diff --git a/db/src/test/resources/application-db.yml b/db/src/test/resources/application-db.yml new file mode 100644 index 0000000..5a9f4e0 --- /dev/null +++ b/db/src/test/resources/application-db.yml @@ -0,0 +1,10 @@ +--- +spring: + datasource: + url: jdbc:tc:postgresql:14-alpine:///test + driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver + hikari: + maximum-pool-size: 2 + flyway: + enabled: true + locations: classpath:migration/structure, classpath:migration/data diff --git a/db/src/test/resources/sql/insert_city.sql b/db/src/test/resources/sql/insert_city.sql new file mode 100644 index 0000000..a2b44aa --- /dev/null +++ b/db/src/test/resources/sql/insert_city.sql @@ -0,0 +1,3 @@ +insert into "city" (id, guid, name, created_at) values + (1000, '21a1a3a8-621a-40f7-b64f-7e118aa241b9', 'Tokyo', '2025-01-01 12:10:05 +00:00') +; diff --git a/db/src/test/resources/sql/insert_customer.sql b/db/src/test/resources/sql/insert_customer.sql new file mode 100644 index 0000000..709c0c1 --- /dev/null +++ b/db/src/test/resources/sql/insert_customer.sql @@ -0,0 +1,3 @@ +insert into "customer" (id, guid, name, city_id, created_at) values + (1000, '823c50de-4c81-49bd-a69a-2d52be42b728', 'Customer', 1000, '2025-01-01 12:10:05 +00:00') +; diff --git a/db/src/test/resources/sql/insert_order.sql b/db/src/test/resources/sql/insert_order.sql new file mode 100644 index 0000000..f64e19d --- /dev/null +++ b/db/src/test/resources/sql/insert_order.sql @@ -0,0 +1,3 @@ +insert into "order" (id, guid, customer_id, created_at) values + (1000, '2c960a08-7187-4e91-9ef3-275c91b1342c', 1000, '2025-01-01 12:10:05 +00:00') +; diff --git a/db/src/test/resources/sql/insert_order_product.sql b/db/src/test/resources/sql/insert_order_product.sql new file mode 100644 index 0000000..85d62e1 --- /dev/null +++ b/db/src/test/resources/sql/insert_order_product.sql @@ -0,0 +1,3 @@ +insert into "order_product" (guid, order_id, product_id, created_at) values + ('930f54e2-c60d-448e-83b1-0d259ff2c2d3', 1000, 1000, '2025-01-01 12:10:05 +00:00') +; diff --git a/db/src/test/resources/sql/insert_product.sql b/db/src/test/resources/sql/insert_product.sql new file mode 100644 index 0000000..7a6f052 --- /dev/null +++ b/db/src/test/resources/sql/insert_product.sql @@ -0,0 +1,3 @@ +insert into "product" (id, guid, name, description, price, created_at) values + (1000, '1fb5c7e4-8ce2-43b8-8ca7-1089b04959b9', 'product', 'description', 10000, '2025-01-01 12:10:05 +00:00') +; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 09ba780..fde85b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,9 +42,10 @@ testcontainers-postgresql = { module = "org.testcontainers:postgresql", version. tracing = ["micrometer-bridge-otel", "otel-exporter"] [plugins] +kotlin-kover = { id = "org.jetbrains.kotlinx.kover", version = "0.8.3" } +kotlin-jpa = { id = "org.jetbrains.kotlin.plugin.jpa", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -kotlin-jpa = { id = "org.jetbrains.kotlin.plugin.jpa", version.ref = "kotlin" } kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } spring-dependencyManagement = { id = "io.spring.dependency-management", version = "1.1.6"} diff --git a/settings.gradle.kts b/settings.gradle.kts index dae155f..2cbfc2f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,2 @@ rootProject.name = "demo" +include("db")