diff --git a/build.gradle.kts b/build.gradle.kts index f69f1d27..1f93f32a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,3 @@ -import org.gradle.kotlin.dsl.libs import org.gradle.plugins.ide.idea.model.IdeaModel import org.jetbrains.dokka.gradle.DokkaMultiModuleTask import org.jetbrains.kotlin.gradle.dsl.JvmTarget @@ -10,13 +9,12 @@ plugins { alias(libs.plugins.testLogger) alias(libs.plugins.kover) alias(libs.plugins.detekt) - alias(libs.plugins.binaryCompatibilityValidator) id("stove-publishing") apply false idea java } group = "com.trendyol" -version = CI.version(project) +version = version() allprojects { extra.set("dokka.outputDirectory", rootDir.resolve("docs")) @@ -42,21 +40,41 @@ kover { } } } -val related = subprojects.of("lib", "spring", "examples", "ktor") +val related = subprojects.of("lib", "spring", "examples", "ktor", "micronaut-starter") dependencies { related.forEach { kover(it) } } -subprojects.of("lib", "spring", "examples", "ktor") { +subprojects.of("lib", "spring", "examples", "ktor", "micronaut-starter") { apply { plugin("kotlin") - plugin(rootProject.libs.plugins.spotless.get().pluginId) - plugin(rootProject.libs.plugins.dokka.get().pluginId) - plugin(rootProject.libs.plugins.testLogger.get().pluginId) - plugin(rootProject.libs.plugins.kover.get().pluginId) - plugin(rootProject.libs.plugins.detekt.get().pluginId) + plugin( + rootProject.libs.plugins.spotless + .get() + .pluginId + ) + plugin( + rootProject.libs.plugins.dokka + .get() + .pluginId + ) + plugin( + rootProject.libs.plugins.testLogger + .get() + .pluginId + ) + plugin( + rootProject.libs.plugins.kover + .get() + .pluginId + ) + plugin( + rootProject.libs.plugins.detekt + .get() + .pluginId + ) plugin("idea") } @@ -76,13 +94,12 @@ subprojects.of("lib", "spring", "examples", "ktor") { testImplementation(libs.kotest.runner.junit5) testImplementation(libs.kotest.framework.api) testImplementation(libs.kotest.property) - testImplementation(libs.kotest.arrow) detektPlugins(libs.detekt.formatting) } spotless { kotlin { - ktlint(libs.versions.ktlint.get()).setEditorConfigPath(rootProject.layout.projectDirectory.file(".editorconfig")) + ktlint().setEditorConfigPath(rootProject.layout.projectDirectory.file(".editorconfig")) targetExclude("build/", "generated/", "out/") targetExcludeIfContentContains("generated") targetExcludeIfContentContainsRegex("generated.*") @@ -141,10 +158,11 @@ val publishedProjects = listOf( "stove-testing-e2e-redis", "stove-ktor-testing-e2e", "stove-spring-testing-e2e", - "stove-spring-testing-e2e-kafka" + "stove-spring-testing-e2e-kafka", + "stove-micronaut-testing-e2e" ) -subprojects.of("lib", "spring", "ktor", filter = { p -> publishedProjects.contains(p.name) }) { +subprojects.of("lib", "spring", "ktor", "micronaut-starter", filter = { p -> publishedProjects.contains(p.name) }) { apply { plugin("java") plugin("stove-publishing") @@ -160,3 +178,11 @@ tasks.withType().configureEach { outputDirectory.set(file(rootDir.resolve("docs/source"))) } +fun version(): String = when { + System.getenv("SNAPSHOT") != null -> { + println("SNAPSHOT: ${System.getenv("SNAPSHOT")}") + project.properties["snapshot"].toString() + } + + else -> project.properties["version"].toString() +} diff --git a/examples/micronaut-example/.gitignore b/examples/micronaut-example/.gitignore new file mode 100644 index 00000000..5a03bc30 --- /dev/null +++ b/examples/micronaut-example/.gitignore @@ -0,0 +1,15 @@ +Thumbs.db +.DS_Store +.gradle +build/ +target/ +out/ +.micronaut/ +.idea +*.iml +*.ipr +*.iws +.project +.settings +.classpath +.factorypath diff --git a/examples/micronaut-example/build.gradle.kts b/examples/micronaut-example/build.gradle.kts new file mode 100644 index 00000000..2f049d5e --- /dev/null +++ b/examples/micronaut-example/build.gradle.kts @@ -0,0 +1,81 @@ +@file:Suppress("UnstableApiUsage", "DSL_SCOPE_VIOLATION") + +plugins { + kotlin("jvm") version libs.versions.kotlin + id("org.jetbrains.kotlin.plugin.allopen") version libs.versions.kotlin + kotlin("plugin.serialization") version libs.versions.kotlin + application + idea + id("com.google.devtools.ksp") version "1.9.25-1.0.20" + id("com.github.johnrengelman.shadow") version "8.1.1" + id("io.micronaut.application") version "4.4.3" + id("io.micronaut.aot") version "4.4.3" +} + +repositories { + mavenCentral() + maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } +} + +dependencies { + runtimeOnly("org.yaml:snakeyaml:2.1") + implementation(platform("io.micronaut.platform:micronaut-parent:4.7.1")) + ksp("io.micronaut:micronaut-http-validation") + ksp("io.micronaut.serde:micronaut-serde-processor") + implementation("io.micronaut.kotlin:micronaut-kotlin-runtime") + implementation("io.micronaut.serde:micronaut-serde-jackson") + implementation("io.micronaut:micronaut-http-client") + implementation("io.micronaut:micronaut-http-server-netty") + implementation("io.micronaut:micronaut-inject") + implementation("io.micronaut:micronaut-core") + runtimeOnly("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("com.couchbase.client:metrics-micrometer:0.7.5") + implementation("io.micronaut.configuration:micronaut-micrometer-core:1.3.1") + implementation("org.apache.kafka:kafka-clients") + implementation(libs.kotlinx.reactor) + implementation(libs.kotlinx.core) + implementation(libs.kotlinx.reactive) + implementation(libs.couchbase.client) + implementation(libs.couchbase.client.metrics) + implementation(libs.jackson.kotlin) + implementation(libs.kotlinx.slf4j) +} + +dependencies { + testImplementation(libs.kotest.property) + testImplementation(libs.kotest.runner.junit5) + testImplementation(projects.stove.lib.stoveTestingE2eHttp) + testImplementation(projects.stove.lib.stoveTestingE2eWiremock) + testImplementation(projects.stove.lib.stoveTestingE2eCouchbase) + testImplementation(projects.stove.lib.stoveTestingE2eElasticsearch) + testImplementation(projects.stove.starters.micronautStarter.stoveMicronautTestingE2e) +} + +application { + mainClass = "stove.micronaut.example.ApplicationKt" +} + +graalvmNative.toolchainDetection = false + +java { + sourceCompatibility = JavaVersion.toVersion("17") +} + +micronaut { + runtime("netty") + testRuntime("kotest5") + processing { + incremental(true) + annotations("stove.micronaut.example.*") + } + aot { + optimizeServiceLoading = false + convertYamlToJava = false + precomputeOperations = true + cacheEnvironment = true + optimizeClassLoading = true + deduceEnvironment = true + optimizeNetty = true + replaceLogbackXml = true + } +} diff --git a/examples/micronaut-example/gradle/wrapper/gradle-wrapper.jar b/examples/micronaut-example/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..a4b76b95 Binary files /dev/null and b/examples/micronaut-example/gradle/wrapper/gradle-wrapper.jar differ diff --git a/examples/micronaut-example/gradle/wrapper/gradle-wrapper.properties b/examples/micronaut-example/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..df97d72b --- /dev/null +++ b/examples/micronaut-example/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/Application.kt b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/Application.kt new file mode 100644 index 00000000..1141cb95 --- /dev/null +++ b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/Application.kt @@ -0,0 +1,28 @@ +package stove.micronaut.example + +import io.micronaut.context.ApplicationContext +import io.micronaut.runtime.EmbeddedApplication + +fun main(args: Array) { + run(args) +} + +fun run( + args: Array, + init: ApplicationContext.() -> Unit = {} +): ApplicationContext { + val context = ApplicationContext + .builder() + .args(*args) + .build() + .also(init) + .start() + + context.findBean(EmbeddedApplication::class.java).ifPresent { app -> + if (!app.isRunning) { + app.start() + } + } + + return context +} diff --git a/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/domain/Product.kt b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/domain/Product.kt new file mode 100644 index 00000000..53fb4ad0 --- /dev/null +++ b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/domain/Product.kt @@ -0,0 +1,26 @@ +package stove.micronaut.example.application.domain + +import io.micronaut.serde.annotation.Serdeable +import java.util.* + +@Serdeable +data class Product( + val id: String, + val name: String, + val supplierId: Long, + val isBlacklist: Boolean, + val createdDate: Date +) { + companion object { + + fun new(id: String, name: String, supplierId: Long, isBlacklist: Boolean): Product { + return Product( + id = id, + name = name, + supplierId = supplierId, + createdDate = Date(), + isBlacklist = isBlacklist + ) + } + } +} diff --git a/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/repository/ProductRepository.kt b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/repository/ProductRepository.kt new file mode 100644 index 00000000..d78f2b56 --- /dev/null +++ b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/repository/ProductRepository.kt @@ -0,0 +1,8 @@ +package stove.micronaut.example.application.repository + +import stove.micronaut.example.application.domain.Product + +interface ProductRepository { + suspend fun save(product: Product): Product + suspend fun findById(id: Long): Product? +} diff --git a/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/services/ProductService.kt b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/services/ProductService.kt new file mode 100644 index 00000000..ee77a9a9 --- /dev/null +++ b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/services/ProductService.kt @@ -0,0 +1,19 @@ +package stove.micronaut.example.application.services + +import jakarta.inject.Singleton +import stove.micronaut.example.application.domain.Product +import stove.micronaut.example.application.repository.ProductRepository +import stove.micronaut.example.infrastructure.http.SupplierHttpService + +@Singleton +class ProductService( + private val productRepository: ProductRepository, + private val supplierHttpService: SupplierHttpService +) { + suspend fun createProduct(id: String, productName: String, supplierId: Long): Product { + val supplier = supplierHttpService.getSupplierPermission(supplierId) + val product = Product.new(id, productName, supplierId, supplier!!.isBlacklisted) + productRepository.save(product) + return product + } +} diff --git a/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/services/SupplierService.kt b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/services/SupplierService.kt new file mode 100644 index 00000000..c80f2bf4 --- /dev/null +++ b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/services/SupplierService.kt @@ -0,0 +1,13 @@ +package stove.micronaut.example.application.services + +import io.micronaut.serde.annotation.Serdeable + +@Serdeable +data class SupplierPermission( + val id: Long, + val isBlacklisted: Boolean +) + +interface SupplierService { + suspend fun getSupplierPermission(supplierId: Long): SupplierPermission? +} diff --git a/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/api/ProductController.kt b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/api/ProductController.kt new file mode 100644 index 00000000..ba3f925b --- /dev/null +++ b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/api/ProductController.kt @@ -0,0 +1,25 @@ +package stove.micronaut.example.infrastructure.api + +import io.micronaut.http.annotation.* +import stove.micronaut.example.application.domain.Product +import stove.micronaut.example.application.services.ProductService +import stove.micronaut.example.infrastructure.api.model.request.CreateProductRequest + +@Controller("/products") +class ProductController( + private val productService: ProductService +) { + @Get("/index") + fun get( + @QueryValue keyword: String = "default" + ): String = "Hi from Stove framework with $keyword" + + @Post("/create") + suspend fun createProduct( + @Body request: CreateProductRequest + ): Product = productService.createProduct( + id = request.id, + productName = request.name, + supplierId = request.supplierId + ) +} diff --git a/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/api/model/request/CreateProductRequest.kt b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/api/model/request/CreateProductRequest.kt new file mode 100644 index 00000000..9925661a --- /dev/null +++ b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/api/model/request/CreateProductRequest.kt @@ -0,0 +1,10 @@ +package stove.micronaut.example.infrastructure.api.model.request + +import io.micronaut.serde.annotation.Serdeable + +@Serdeable +data class CreateProductRequest( + val id: String, + val name: String, + val supplierId: Long +) diff --git a/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/couchbase/CouchbaseConfiguration.kt b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/couchbase/CouchbaseConfiguration.kt new file mode 100644 index 00000000..43814989 --- /dev/null +++ b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/couchbase/CouchbaseConfiguration.kt @@ -0,0 +1,64 @@ +package stove.micronaut.example.infrastructure.couchbase + +import com.couchbase.client.java.* +import com.couchbase.client.java.Collection +import com.couchbase.client.java.codec.JacksonJsonSerializer +import com.couchbase.client.java.env.ClusterEnvironment +import com.couchbase.client.java.json.JsonValueModule +import com.fasterxml.jackson.databind.ObjectMapper +import io.micronaut.context.annotation.* +import jakarta.annotation.PreDestroy +import jakarta.inject.Singleton +import java.time.Duration + +@Factory +class CouchbaseConfiguration( + private val couchbaseProperties: CouchbaseProperties +) { + companion object { + val objectMapper: ObjectMapper = + ObjectMapper() + .findAndRegisterModules() + .registerModule(JsonValueModule()) + } + + @Primary + @Context + fun clusterEnvironment(): ClusterEnvironment { + val cbSerializer = JacksonJsonSerializer.create(objectMapper) + return ClusterEnvironment + .builder() + .timeoutConfig { + it + .kvTimeout(Duration.ofMillis(couchbaseProperties.kvTimeout)) + .connectTimeout(Duration.ofMillis(couchbaseProperties.connectTimeout)) + .queryTimeout(Duration.ofMillis(couchbaseProperties.queryTimeout)) + .viewTimeout(Duration.ofMillis(couchbaseProperties.viewTimeout)) + }.jsonSerializer(cbSerializer) + .build() + } + + @Primary + @Singleton + fun cluster(clusterEnvironment: ClusterEnvironment): Cluster { + val clusterOptions = ClusterOptions + .clusterOptions(couchbaseProperties.username, couchbaseProperties.password) + .environment(clusterEnvironment) + + return Cluster.connect(couchbaseProperties.hosts.joinToString(","), clusterOptions) + } + + @Primary + @Singleton + fun bucket(cluster: Cluster): Bucket = cluster.bucket(couchbaseProperties.bucketName) + + @Primary + @Singleton + fun productCouchbaseCollection(bucket: Bucket): Collection = bucket.defaultCollection() + + @PreDestroy + fun cleanup(cluster: Cluster, clusterEnvironment: ClusterEnvironment) { + cluster.disconnect() + clusterEnvironment.shutdown() + } +} diff --git a/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/couchbase/CouchbaseProperties.kt b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/couchbase/CouchbaseProperties.kt new file mode 100644 index 00000000..75e5918a --- /dev/null +++ b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/couchbase/CouchbaseProperties.kt @@ -0,0 +1,15 @@ +package stove.micronaut.example.infrastructure.couchbase + +import io.micronaut.context.annotation.* + +@ConfigurationProperties("couchbase") +class CouchbaseProperties { + var username: String? = null + var password: String? = null + var bucketName: String = "" + var hosts: List = listOf() + var kvTimeout: Long = 0 + var connectTimeout: Long = 0 + var queryTimeout: Long = 0 + var viewTimeout: Long = 0 +} diff --git a/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/couchbase/ObjectMapperConfig.kt b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/couchbase/ObjectMapperConfig.kt new file mode 100644 index 00000000..8df6bae9 --- /dev/null +++ b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/couchbase/ObjectMapperConfig.kt @@ -0,0 +1,29 @@ +package stove.micronaut.example.infrastructure.couchbase + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.module.kotlin.KotlinModule +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Factory + +@Factory +class ObjectMapperConfig { + + companion object { + fun createObjectMapperWithDefaults(): ObjectMapper { + val isoInstantModule = SimpleModule() + return ObjectMapper() + .registerModule(KotlinModule.Builder().build()) + .registerModule(isoInstantModule) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + } + } + + @Bean + fun objectMapper(): ObjectMapper { + return createObjectMapperWithDefaults() + } +} diff --git a/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/http/SupplierHttpService.kt b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/http/SupplierHttpService.kt new file mode 100644 index 00000000..362914d2 --- /dev/null +++ b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/http/SupplierHttpService.kt @@ -0,0 +1,28 @@ +package stove.micronaut.example.infrastructure.http + +import io.micronaut.http.annotation.Get +import io.micronaut.http.client.annotation.Client +import io.micronaut.websocket.exceptions.WebSocketClientException +import jakarta.inject.Singleton +import stove.micronaut.example.application.services.SupplierPermission +import stove.micronaut.example.application.services.SupplierService + +@Singleton +class SupplierHttpService( + private val supplierHttpClient: SupplierHttpClient +) : SupplierService { + override suspend fun getSupplierPermission(supplierId: Long): SupplierPermission? = try { + val response = supplierHttpClient.getSupplierPermission(supplierId) + println("API Response: $response") // Yanıtı konsola yazdır + response + } catch (e: WebSocketClientException) { + println("Error fetching supplier permission: ${e.message}") + null + } +} + +@Client(id = "lookup-api") +interface SupplierHttpClient { + @Get("/v2/suppliers/{supplierId}?storeFrontId=1") + suspend fun getSupplierPermission(supplierId: Long): SupplierPermission +} diff --git a/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/persistence/ProductCBRepository.kt b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/persistence/ProductCBRepository.kt new file mode 100644 index 00000000..61eecc63 --- /dev/null +++ b/examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/persistence/ProductCBRepository.kt @@ -0,0 +1,18 @@ +package stove.micronaut.example.infrastructure.persistence + +import com.couchbase.client.java.Collection +import jakarta.inject.Singleton +import stove.micronaut.example.application.domain.Product +import stove.micronaut.example.application.repository.ProductRepository + +@Singleton +class ProductCBRepository( + private val productCouchbaseCollection: Collection +) : ProductRepository { + override suspend fun save(product: Product): Product { + productCouchbaseCollection.insert(product.id, product) + return product + } + + override suspend fun findById(id: Long): Product? = productCouchbaseCollection.get(id.toString()).contentAs(Product::class.java) +} diff --git a/examples/micronaut-example/src/main/resources/application.yml b/examples/micronaut-example/src/main/resources/application.yml new file mode 100644 index 00000000..d645d6e0 --- /dev/null +++ b/examples/micronaut-example/src/main/resources/application.yml @@ -0,0 +1,28 @@ +micronaut: + application: + name: micronaut-example + server: + port: 8080 + http: + services: + lookup-api: + url: http://localhost:7078 + connect-timeout: 2s + read-timeout: 22s + +micrometer: + metrics: + enabled: true + common-tags: + application: "micronaut-example" + +couchbase: + hosts: localhost + username: username + password: password + bucketName: Stove + kvTimeout: 3000 + connectTimeout: 15000 + queryTimeout: 5000 + viewTimeout: 5000 + diff --git a/examples/micronaut-example/src/main/resources/logback.xml b/examples/micronaut-example/src/main/resources/logback.xml new file mode 100644 index 00000000..2d77bdab --- /dev/null +++ b/examples/micronaut-example/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + diff --git a/examples/micronaut-example/src/test/kotlin/com/stove/micronaut/example/e2e/ProductControllerTest.kt b/examples/micronaut-example/src/test/kotlin/com/stove/micronaut/example/e2e/ProductControllerTest.kt new file mode 100644 index 00000000..fb242004 --- /dev/null +++ b/examples/micronaut-example/src/test/kotlin/com/stove/micronaut/example/e2e/ProductControllerTest.kt @@ -0,0 +1,59 @@ +package com.stove.micronaut.example.e2e + +import arrow.core.some +import com.trendyol.stove.testing.e2e.couchbase.couchbase +import com.trendyol.stove.testing.e2e.http.http +import com.trendyol.stove.testing.e2e.system.TestSystem +import com.trendyol.stove.testing.e2e.wiremock.wiremock +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import stove.micronaut.example.application.domain.Product +import stove.micronaut.example.application.services.SupplierPermission +import stove.micronaut.example.infrastructure.api.model.request.CreateProductRequest + +class ProductControllerTest : + FunSpec({ + + test("index should be reachable") { + TestSystem.validate { + http { + get("/products/index", queryParams = mapOf("keyword" to "index")) { actual -> + actual shouldContain "Hi from Stove framework with index" + println(actual) + } + } + } + } + + test("should create a product in Couchbase when a product is created") { + val id = "RANDOM0001" + val request = CreateProductRequest(id = id, name = "Deneme", supplierId = 120688) + val supplierMock = SupplierPermission(id = 120688, isBlacklisted = false) + + TestSystem.validate { + + wiremock { + mockGet( + "/v2/suppliers/${supplierMock.id}?storeFrontId=1", + statusCode = 200, + responseBody = supplierMock.some() + ) + } + http { + postAndExpectJson("/products/create", body = request.some()) { actual -> + actual.supplierId shouldBe 120688 + actual.name shouldBe "Deneme" + } + } + couchbase { + shouldGet(request.id) { + it.name shouldBe request.name + it.id shouldBe request.id + it.supplierId shouldBe request.supplierId + it.isBlacklist shouldBe false + } + } + } + } + }) diff --git a/examples/micronaut-example/src/test/kotlin/com/stove/micronaut/example/e2e/TestSystemConfig.kt b/examples/micronaut-example/src/test/kotlin/com/stove/micronaut/example/e2e/TestSystemConfig.kt new file mode 100644 index 00000000..850b2188 --- /dev/null +++ b/examples/micronaut-example/src/test/kotlin/com/stove/micronaut/example/e2e/TestSystemConfig.kt @@ -0,0 +1,66 @@ +package com.stove.micronaut.example.e2e + +import com.trendyol.stove.testing.* +import com.trendyol.stove.testing.e2e.couchbase.* +import com.trendyol.stove.testing.e2e.http.* +import com.trendyol.stove.testing.e2e.system.TestSystem +import com.trendyol.stove.testing.e2e.wiremock.* +import io.kotest.core.config.AbstractProjectConfig +import org.slf4j.* +import stove.micronaut.example.infrastructure.couchbase.ObjectMapperConfig + +class TestSystemConfig : AbstractProjectConfig() { + private val logger: Logger = LoggerFactory.getLogger("WireMockMonitor") + + @Suppress("LongMethod") + override suspend fun beforeProject() { + val objectMapper = ObjectMapperConfig.createObjectMapperWithDefaults() + TestSystem() + .with { + httpClient { + HttpClientSystemOptions( + baseUrl = "http://localhost:8080" + ) + } + couchbase { + CouchbaseSystemOptions( + "Stove", + objectMapper = objectMapper, + containerOptions = CouchbaseContainerOptions(tag = "latest") { + withStartupAttempts(3) + }, + configureExposedConfiguration = { cfg -> + listOf( + "couchbase.hosts=${cfg.hostsWithPort}", + "couchbase.username=${cfg.username}", + "couchbase.password=${cfg.password}" + ) + } + ) + } + bridge() + wiremock { + WireMockSystemOptions( + port = 7078, + removeStubAfterRequestMatched = true, + afterRequest = { e, _ -> + logger.info(e.request.toString()) + } + ) + } + micronaut( + runner = { parameters -> + stove.micronaut.example.run(parameters) { + } + }, + withParameters = listOf( + "server.port=8080", + "logging.level.root=info", + "logging.level.org.springframework.web=info" + ) + ) + }.run() + } + + override suspend fun afterProject(): Unit = TestSystem.stop() +} diff --git a/gradle.properties b/gradle.properties index 9de20adb..4a6ed33e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,6 @@ projectDescription=The easiest way of e2e testing in Kotlin projectUrl=https://github.com/Trendyol/stove licenceUrl=https://github.com/Trendyol/stove/blob/master/LICENCE licence=Apache-2.0 license -snapshot=1.0.0 -version=0.15.0 - - +snapshot=1.0.0-SNAPSHOT +version=0.14.2 +micronautVersion=4.4.3 diff --git a/settings.gradle.kts b/settings.gradle.kts index fd6e19bd..cf6eb5a0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,5 @@ @file:Suppress("UnstableApiUsage") - enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") - rootProject.name = "stove" include( "lib:stove-testing-e2e", @@ -16,18 +14,18 @@ include( "lib:stove-testing-e2e-redis", "lib:stove-testing-e2e-mongodb" ) - include( "starters:ktor:stove-ktor-testing-e2e", "starters:spring:stove-spring-testing-e2e", - "starters:spring:stove-spring-testing-e2e-kafka" + "starters:spring:stove-spring-testing-e2e-kafka", + "starters:micronaut-starter:stove-micronaut-testing-e2e" ) - include( "examples:spring-example", "examples:spring-standalone-example", "examples:ktor-example", - "examples:spring-streams-example" + "examples:spring-streams-example", + "examples:micronaut-example" ) dependencyResolutionManagement { repositories { diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/.idea/.gitignore b/starters/micronaut-starter/stove-micronaut-testing-e2e/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/.idea/gradle.xml b/starters/micronaut-starter/stove-micronaut-testing-e2e/.idea/gradle.xml new file mode 100644 index 00000000..39ec4f56 --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/.idea/gradle.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/.idea/misc.xml b/starters/micronaut-starter/stove-micronaut-testing-e2e/.idea/misc.xml new file mode 100644 index 00000000..6ed36dd3 --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/.idea/vcs.xml b/starters/micronaut-starter/stove-micronaut-testing-e2e/.idea/vcs.xml new file mode 100644 index 00000000..c2365ab1 --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/build.gradle.kts b/starters/micronaut-starter/stove-micronaut-testing-e2e/build.gradle.kts new file mode 100644 index 00000000..1288da28 --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/build.gradle.kts @@ -0,0 +1,13 @@ +dependencies { + api(projects.lib.stoveTestingE2e) + implementation("io.micronaut:micronaut-core") // Temel Micronaut bağımlılığı + implementation("io.micronaut.kotlin:micronaut-kotlin-runtime:4.5.0") + testImplementation("io.micronaut.test:micronaut-test-kotest5:4.0.0") + testImplementation("io.micronaut.test:micronaut-test-kotest5:4.0.0") + testImplementation("io.kotest:kotest-runner-junit5:5.7.2") +} + +repositories { + mavenCentral() + maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } +} diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/gradle/wrapper/gradle-wrapper.jar b/starters/micronaut-starter/stove-micronaut-testing-e2e/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..a4b76b95 Binary files /dev/null and b/starters/micronaut-starter/stove-micronaut-testing-e2e/gradle/wrapper/gradle-wrapper.jar differ diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/gradle/wrapper/gradle-wrapper.properties b/starters/micronaut-starter/stove-micronaut-testing-e2e/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..df97d72b --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/kotlin/com/trendyol/stove/testing/BaseApplicationContextInitializer.kt b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/kotlin/com/trendyol/stove/testing/BaseApplicationContextInitializer.kt new file mode 100644 index 00000000..3a7012c2 --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/kotlin/com/trendyol/stove/testing/BaseApplicationContextInitializer.kt @@ -0,0 +1,38 @@ +package com.trendyol.stove.testing + +import com.trendyol.stove.testing.e2e.system.annotations.StoveDsl +import io.micronaut.context.ApplicationContext +import io.micronaut.context.event.ApplicationEventListener +import io.micronaut.context.event.StartupEvent +import io.micronaut.runtime.Micronaut + +@StoveDsl +abstract class BaseApplicationContextInitializer : ApplicationEventListener { + + private val registrations = mutableListOf<(ApplicationContext) -> Unit>() + + @StoveDsl + fun register(registration: (ApplicationContext) -> Unit): BaseApplicationContextInitializer { + registrations.add(registration) + return this + } + + override fun onApplicationEvent(event: StartupEvent) { + val applicationContext = event.source as ApplicationContext + onStartup(applicationContext) + registrations.forEach { it(applicationContext) } + } + + protected open fun onStartup(applicationContext: ApplicationContext) { + // Custom initialization code can be added here + } + + companion object { + @JvmStatic fun main(args: Array) { + Micronaut.build() + .args(*args) + .packages("your.base.package.here") + .start() + } + } +} diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/kotlin/com/trendyol/stove/testing/MicronautApplicationUnderTest.kt b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/kotlin/com/trendyol/stove/testing/MicronautApplicationUnderTest.kt new file mode 100644 index 00000000..6a213a45 --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/kotlin/com/trendyol/stove/testing/MicronautApplicationUnderTest.kt @@ -0,0 +1,56 @@ +package com.trendyol.stove.testing + +import com.trendyol.stove.testing.e2e.system.* +import com.trendyol.stove.testing.e2e.system.abstractions.* +import com.trendyol.stove.testing.e2e.system.annotations.StoveDsl +import io.micronaut.context.* +import kotlinx.coroutines.* + +@StoveDsl +internal fun TestSystem.systemUnderTest( + runner: Runner, + withParameters: List = listOf() +): ReadyTestSystem { + this.applicationUnderTest(MicronautApplicationUnderTest(this, runner, withParameters)) + return this +} + +@StoveDsl +fun WithDsl.micronaut( + runner: Runner, + withParameters: List = listOf() +): ReadyTestSystem = this.testSystem.systemUnderTest(runner, withParameters) + +@StoveDsl +class MicronautApplicationUnderTest( + private val testSystem: TestSystem, + private val runner: Runner, + private val parameters: List +) : ApplicationUnderTest { + private lateinit var application: ApplicationContext + + companion object { + private const val DELAY = 500L + } + + override suspend fun start(configurations: List): ApplicationContext = coroutineScope { + val allConfigurations = (configurations + defaultConfigurations() + parameters).map { "--$it" }.toTypedArray() + application = runner(allConfigurations) + while (!application.isRunning) { + delay(DELAY) + continue + } + testSystem.activeSystems + .map { it.value } + .filterIsInstance>() + .map { async(context = Dispatchers.IO) { it.afterRun(application) } } + .awaitAll() + application + } + + override suspend fun stop() { + application.stop() + } + + private fun defaultConfigurations(): Array = arrayOf("test-system=true") +} diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/kotlin/com/trendyol/stove/testing/MicronautBridgeSystem.kt b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/kotlin/com/trendyol/stove/testing/MicronautBridgeSystem.kt new file mode 100644 index 00000000..0b70ebd2 --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/kotlin/com/trendyol/stove/testing/MicronautBridgeSystem.kt @@ -0,0 +1,16 @@ +package com.trendyol.stove.testing + +import com.trendyol.stove.testing.e2e.system.* +import com.trendyol.stove.testing.e2e.system.abstractions.* +import com.trendyol.stove.testing.e2e.system.annotations.StoveDsl +import io.micronaut.context.ApplicationContext +import kotlin.reflect.KClass + +@StoveDsl +class MicronautBridgeSystem( + override val testSystem: TestSystem +) : PluggedSystem, AfterRunAwareWithContext, BridgeSystem(testSystem) { + override fun get(klass: KClass): D = ctx.getBean(klass.java) +} +@StoveDsl +fun WithDsl.bridge(): TestSystem = this.testSystem.withBridgeSystem(MicronautBridgeSystem(this.testSystem)) diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/resources/application.properties b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/resources/application.properties new file mode 100644 index 00000000..de971bae --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/resources/application.properties @@ -0,0 +1,2 @@ +#Mon Nov 18 19:19:36 UTC 2024 +micronaut.application.name=stove-micronaut-testing-e2e diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/resources/logback.xml b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/resources/logback.xml new file mode 100644 index 00000000..2d77bdab --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/src/test/kotlin/com/trendyol/stove/testing/BridgeSystemKtTest.kt b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/test/kotlin/com/trendyol/stove/testing/BridgeSystemKtTest.kt new file mode 100644 index 00000000..36a72b6f --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/test/kotlin/com/trendyol/stove/testing/BridgeSystemKtTest.kt @@ -0,0 +1,100 @@ +package com.trendyol.stove.testing + +import com.fasterxml.jackson.databind.ObjectMapper +import io.kotest.core.config.AbstractProjectConfig +import io.kotest.core.spec.style.* +import io.kotest.matchers.shouldBe +import io.micronaut.context.annotation.* +import io.micronaut.runtime.EmbeddedApplication +import io.micronaut.runtime.Micronaut +import jakarta.inject.Singleton +import kotlinx.coroutines.delay +import java.time.Instant +import kotlin.time.Duration.Companion.seconds + +class BridgeSystemTests( + private val exampleService: ExampleService, + private val getUtcNow: GetUtcNow, + private val parameterCollector: ApplicationParameterCollector, + private val embeddedApplication: EmbeddedApplication<*> +) : FunSpec({ + + test("bridge to application") { + embeddedApplication.isRunning shouldBe true + + exampleService.whatIsTheTime() shouldBe GetUtcNow.frozenTime + getUtcNow() shouldBe GetUtcNow.frozenTime + + parameterCollector.parameters shouldBe listOf( + "--test-system=true", + "--context=SetupOfBridgeSystemTests" + ) + + delay(5.seconds) + } + + test("resolve multiple") { + getUtcNow() shouldBe GetUtcNow.frozenTime + exampleService.whatIsTheTime() shouldBe GetUtcNow.frozenTime + + parameterCollector.parameters shouldBe listOf( + "--test-system=true", + "--context=SetupOfBridgeSystemTests" + ) + } + }) + +@Factory +class TestAppConfig { + @Singleton + fun objectMapper(): ObjectMapper = ObjectMapper() + + @Singleton + fun getUtcNow(): GetUtcNow = SystemTimeGetUtcNow() + + @Singleton + fun exampleService(getUtcNow: GetUtcNow): ExampleService = ExampleService(getUtcNow) + + @Singleton + fun applicationParameterCollector(environment: io.micronaut.context.env.Environment): ApplicationParameterCollector = ApplicationParameterCollector(environment) +} + +fun interface GetUtcNow { + companion object { + val frozenTime: Instant = Instant.parse("2021-01-01T00:00:00Z") + } + + operator fun invoke(): Instant +} + +class SystemTimeGetUtcNow : GetUtcNow { + override fun invoke(): Instant = GetUtcNow.frozenTime +} + +@Singleton +class ExampleService( + private val getUtcNow: GetUtcNow +) { + fun whatIsTheTime(): Instant = getUtcNow() +} + +@Singleton +class ApplicationParameterCollector( + private val environment: io.micronaut.context.env.Environment +) { + val parameters: List + get() = environment.activeNames.toList() +} + +class Setup : AbstractProjectConfig() { + override suspend fun beforeProject() { + Micronaut + .build() + .args("--test-system=true", "--context=SetupOfBridgeSystemTests") + .start() + } + + override suspend fun afterProject() { + // Uygulama durdurma işlemi yapılabilir + } +} diff --git a/starters/micronaut-starter/stove-micronaut-testing-e2e/src/test/kotlin/io/kotest/provided/ProjectConfig.kt b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/test/kotlin/io/kotest/provided/ProjectConfig.kt new file mode 100644 index 00000000..a328bf46 --- /dev/null +++ b/starters/micronaut-starter/stove-micronaut-testing-e2e/src/test/kotlin/io/kotest/provided/ProjectConfig.kt @@ -0,0 +1,8 @@ +package io.kotest.provided + +import io.kotest.core.config.AbstractProjectConfig +import io.micronaut.test.extensions.kotest5.MicronautKotest5Extension + +object ProjectConfig : AbstractProjectConfig() { + override fun extensions() = listOf(MicronautKotest5Extension) +}