diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27bd29e..88699cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,9 @@ jobs: # runs-on: macos-latest steps: - uses: actions/checkout@v3 + with: + submodules: recursive + token: ${{ secrets.PAT_TOKEN }} - name: Set up JDK 17 uses: actions/setup-java@v3 @@ -37,6 +40,13 @@ jobs: - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@v1 + # https://github.com/actions/runner-images/issues/675 + - name: Hack sources.list + run: sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu/|http://mirror.arizona.edu/ubuntu/|g' /etc/apt/sources.list + + - name: Install curl-dev + run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev libpq-dev libpq5 + - name: Run Tests uses: gradle/gradle-build-action@v2 with: diff --git a/.gitignore b/.gitignore index e64f94c..fbd9589 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .gradle -build/ +**/build/ !gradle/wrapper/gradle-wrapper.jar !**/src/**/build/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..165db53 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "pgkn"] + path = pgkn + url = git@github.com:crowdproj/pgkn.git diff --git a/build-plugin/build.gradle.kts b/build-plugin/build.gradle.kts index 968d956..6e4496d 100644 --- a/build-plugin/build.gradle.kts +++ b/build-plugin/build.gradle.kts @@ -12,6 +12,10 @@ gradlePlugin { id = "build-kmp" implementationClass = "ru.otus.otuskotlin.marketplace.plugin.BuildPluginMultiplatform" } + register("build-pgContainer") { + id = "build-pgContainer" + implementationClass = "ru.otus.otuskotlin.marketplace.plugin.BuildPluginPgContainer" + } } } @@ -28,5 +32,9 @@ dependencies { implementation(libs.plugin.kotlin) // implementation(libs.plugin.dokka) implementation(libs.plugin.binaryCompatibilityValidator) -// implementation(libs.plugin.mavenPublish) + + implementation(libs.testcontainers.postgres) + implementation(libs.testcontainers.core) + implementation(libs.db.postgres) +// implementation("com.github.docker-java:docker-java-core:3.3.6") } diff --git a/build-plugin/src/main/kotlin/BuildPluginPgContainer.kt b/build-plugin/src/main/kotlin/BuildPluginPgContainer.kt new file mode 100644 index 0000000..a39689a --- /dev/null +++ b/build-plugin/src/main/kotlin/BuildPluginPgContainer.kt @@ -0,0 +1,34 @@ +package ru.otus.otuskotlin.marketplace.plugin + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.testcontainers.containers.PostgreSQLContainer +import org.testcontainers.containers.wait.strategy.Wait + +@Suppress("unused") +internal class BuildPluginPgContainer : Plugin { + val pgDbName = "marketplace_ads" + val pgUsername = "postgres" + val pgPassword = "marketplace-pass" + + private val pgContainer = PostgreSQLContainer("postgres:latest").apply { + withUsername(pgUsername) + withPassword(pgPassword) + withDatabaseName(pgDbName) + waitingFor(Wait.forLogMessage("database system is ready to accept connections", 1)) + } + + + override fun apply(project: Project): Unit = with(project) { + val stopTask = tasks.register("pgStop") { + group = "containers" + pgContainer.stop() + } + tasks.register("pgStart", PgContainerStartTask::class.java) { + pgContainer.start() +// port = pgContainer.getMappedPort(5432) + pgUrl = pgContainer.jdbcUrl + finalizedBy(stopTask) + } + } +} diff --git a/build-plugin/src/main/kotlin/PgContainerStartTask.kt b/build-plugin/src/main/kotlin/PgContainerStartTask.kt new file mode 100644 index 0000000..3763aa4 --- /dev/null +++ b/build-plugin/src/main/kotlin/PgContainerStartTask.kt @@ -0,0 +1,19 @@ +package ru.otus.otuskotlin.marketplace.plugin + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import org.gradle.work.InputChanges + +open class PgContainerStartTask : DefaultTask() { + override fun getGroup(): String = "containers" + +// var port: Int = 5432 + var pgUrl: String = "" + + @TaskAction + fun execute(inputs: InputChanges) { // InputChanges parameter + val msg = if (inputs.isIncremental) "CHANGED inputs are out of date" + else "ALL inputs are out of date" + println(msg) + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 062468a..fd9dbee 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,8 +19,18 @@ subprojects { } tasks { + val buildImages: Task by creating { + dependsOn(gradle.includedBuild("ok-marketplace-be").task(":buildImages")) + } + val e2eTests: Task by creating { + dependsOn(gradle.includedBuild("ok-marketplace-tests").task(":e2eTests")) + mustRunAfter(buildImages) + } + create("check") { group = "verification" - dependsOn(gradle.includedBuild("ok-marketplace-be").task(":check")) +// dependsOn(gradle.includedBuild("ok-marketplace-be").task(":check")) + dependsOn(buildImages) + dependsOn(e2eTests) } } diff --git a/deploy/docker-compose-postgres.yml b/deploy/docker-compose-postgres.yml new file mode 100644 index 0000000..bd55d79 --- /dev/null +++ b/deploy/docker-compose-postgres.yml @@ -0,0 +1,24 @@ +version: '3' +services: + psql: + image: postgres + container_name: postgresql + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + expose: + - "5432" + environment: + POSTGRES_PASSWORD: marketplace-pass + POSTGRES_USER: postgres + POSTGRES_DB: marketplace_ads + healthcheck: + test: [ "CMD-SHELL", "pg_isready" ] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8be0161..996f1af 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,9 @@ kermit = "2.0.3" ktor = "2.3.9" spring-boot = "3.2.0" +liquibase = "4.27.0" +exposed = "0.50.0" + # Docker testcontainers = "1.19.7" muschko = "9.4.0" @@ -83,6 +86,16 @@ kafka-client = { module = "org.apache.kafka:kafka-clients", version = "3.7.0" } # Databases db-cache4k = "io.github.reactivecircus.cache4k:cache4k:0.13.0" +db-postgres = "org.postgresql:postgresql:42.7.3" +db-hikari = "com.zaxxer:HikariCP:5.1.0" +db-exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } +db-exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" } +db-exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } + +# Liquidbase +liquibase-core = {module = "org.liquibase:liquibase-core", version.ref = "liquibase"} +liquibase-picocli = "info.picocli:picocli:4.7.5" +liquibase-snakeyml = "org.yaml:snakeyaml:1.33" # Testing kotest-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } @@ -93,9 +106,11 @@ mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "5.2. testcontainers-core = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" } testcontainers-rabbitmq = { module = "org.testcontainers:rabbitmq", version.ref = "testcontainers" } +testcontainers-postgres = { module = "org.testcontainers:postgresql", version.ref = "testcontainers" } [bundles] kotest = ["kotest-junit5", "kotest-core", "kotest-datatest", "kotest-property"] +exposed = ["db-exposed-core", "db-exposed-dao", "db-exposed-jdbc"] [plugins] kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } @@ -104,6 +119,7 @@ openapi-generator = { id = "org.openapi.generator", version.ref = "openapi-gener crowdproj-generator = { id = "com.crowdproj.generator", version = "0.2.0" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } shadowJar = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } +liquibase = { id = "org.liquibase.gradle", version = "2.2.2" } # Spring spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } diff --git a/ok-marketplace-be/build.gradle.kts b/ok-marketplace-be/build.gradle.kts index 305ef06..f870286 100644 --- a/ok-marketplace-be/build.gradle.kts +++ b/ok-marketplace-be/build.gradle.kts @@ -33,4 +33,10 @@ tasks { dependsOn(subprojects.map { it.getTasksByName(tsk,false)}) } } + + create("buildImages") { + dependsOn(project("ok-marketplace-app-spring").tasks.getByName("bootBuildImage")) + dependsOn(project("ok-marketplace-app-ktor").tasks.getByName("publishImageToLocalRegistry")) + dependsOn(project("ok-marketplace-app-ktor").tasks.getByName("dockerBuildX64Image")) + } } diff --git a/ok-marketplace-be/gradle.properties b/ok-marketplace-be/gradle.properties index 142a5c6..541bd1f 100644 --- a/ok-marketplace-be/gradle.properties +++ b/ok-marketplace-be/gradle.properties @@ -1,6 +1,6 @@ kotlin.code.style=official kotlin.native.ignoreDisabledTargets=true -#kotlin.native.cacheKind.linuxX64=none +kotlin.native.cacheKind.linuxX64=none org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 diff --git a/ok-marketplace-be/ok-marketplace-api-v1-mappers/src/main/kotlin/MappersV1ToTransport.kt b/ok-marketplace-be/ok-marketplace-api-v1-mappers/src/main/kotlin/MappersV1ToTransport.kt index c768470..c07b767 100644 --- a/ok-marketplace-be/ok-marketplace-api-v1-mappers/src/main/kotlin/MappersV1ToTransport.kt +++ b/ok-marketplace-be/ok-marketplace-api-v1-mappers/src/main/kotlin/MappersV1ToTransport.kt @@ -54,7 +54,8 @@ fun MkplContext.toTransportSearch() = AdSearchResponse( fun MkplContext.toTransportOffers() = AdOffersResponse( result = state.toResult(), errors = errors.toTransportErrors(), - ads = adsResponse.toTransportAd() + ad = adResponse.toTransportAd(), + ads = adsResponse.toTransportAd(), ) fun MkplContext.toTransportInit() = AdInitResponse( @@ -75,6 +76,7 @@ private fun MkplAd.toTransportAd(): AdResponseObject = AdResponseObject( adType = adType.toTransportAd(), visibility = visibility.toTransportAd(), permissions = permissionsClient.toTransportAd(), + lock = lock.takeIf { it != MkplAdLock.NONE }?.asString() ) private fun Set.toTransportAd(): Set? = this diff --git a/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonMain/kotlin/MkplAdApiSerializer.kt b/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonMain/kotlin/MkplAdApiSerializer.kt index 7547ca0..8d56553 100644 --- a/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonMain/kotlin/MkplAdApiSerializer.kt +++ b/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonMain/kotlin/MkplAdApiSerializer.kt @@ -2,13 +2,17 @@ package ru.otus.otuskotlin.marketplace.api.v2 +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import ru.otus.otuskotlin.marketplace.api.v2.models.IRequest import ru.otus.otuskotlin.marketplace.api.v2.models.IResponse +@OptIn(ExperimentalSerializationApi::class) @Suppress("JSON_FORMAT_REDUNDANT_DEFAULT") val apiV2Mapper = Json { // ignoreUnknownKeys = true + allowTrailingComma = true } @Suppress("UNCHECKED_CAST") @@ -22,6 +26,13 @@ fun apiV2ResponseSerialize(obj: IResponse): String = fun apiV2ResponseDeserialize(json: String) = apiV2Mapper.decodeFromString(json) as T +inline fun apiV2ResponseSimpleDeserialize(json: String) = + apiV2Mapper.decodeFromString(json) + @Suppress("unused") fun apiV2RequestSerialize(obj: IRequest): String = apiV2Mapper.encodeToString(IRequest.serializer(), obj) + +@Suppress("unused") +inline fun apiV2RequestSimpleSerialize(obj: T): String = + apiV2Mapper.encodeToString(obj) diff --git a/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonMain/kotlin/mappers/MappersV2ToTransport.kt b/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonMain/kotlin/mappers/MappersV2ToTransport.kt index 8f1c548..0f3650a 100644 --- a/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonMain/kotlin/mappers/MappersV2ToTransport.kt +++ b/ok-marketplace-be/ok-marketplace-api-v2-kmp/src/commonMain/kotlin/mappers/MappersV2ToTransport.kt @@ -50,7 +50,8 @@ fun MkplContext.toTransportSearch() = AdSearchResponse( fun MkplContext.toTransportOffers() = AdOffersResponse( result = state.toResult(), errors = errors.toTransportErrors(), - ads = adsResponse.toTransportAd() + ad = adResponse.toTransportAd(), + ads = adsResponse.toTransportAd(), ) fun MkplContext.toTransportInit() = AdInitResponse( diff --git a/ok-marketplace-be/ok-marketplace-app-ktor/build.gradle.kts b/ok-marketplace-be/ok-marketplace-app-ktor/build.gradle.kts index 13ed940..2906736 100644 --- a/ok-marketplace-be/ok-marketplace-app-ktor/build.gradle.kts +++ b/ok-marketplace-be/ok-marketplace-app-ktor/build.gradle.kts @@ -75,8 +75,11 @@ kotlin { implementation(libs.ktor.serialization.json) // DB + implementation(libs.uuid) + implementation(projects.okMarketplaceRepoCommon) implementation(projects.okMarketplaceRepoStubs) implementation(projects.okMarketplaceRepoInmemory) + implementation(projects.okMarketplaceRepoPostgres) // logging implementation(project(":ok-marketplace-api-log1")) @@ -116,7 +119,7 @@ kotlin { implementation(project(":ok-marketplace-api-v1-mappers")) implementation("ru.otus.otuskotlin.marketplace.libs:ok-marketplace-lib-logging-logback") - + implementation(libs.testcontainers.postgres) } } @@ -155,6 +158,7 @@ tasks { into("${this@creating.destDir.get()}") } } + runCommand("apt-get update && apt-get install -y libpq5 && rm -rf /var/lib/apt/lists/*") copyFile(nativeFileX64.name, "/app/") copyFile("application.yaml", "/app/") exposePort(8080) diff --git a/ok-marketplace-be/ok-marketplace-app-ktor/src/appleMain/kotlin/plugins/getDatabaseConf.apple.kt b/ok-marketplace-be/ok-marketplace-app-ktor/src/appleMain/kotlin/plugins/getDatabaseConf.apple.kt new file mode 100644 index 0000000..323cc23 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-app-ktor/src/appleMain/kotlin/plugins/getDatabaseConf.apple.kt @@ -0,0 +1,18 @@ +package ru.otus.otuskotlin.marketplace.app.ktor.plugins + +import io.ktor.server.application.* +import ru.otus.otuskotlin.marketplace.app.ktor.configs.ConfigPaths +import ru.otus.otuskotlin.marketplace.common.repo.IRepoAd + +actual fun Application.getDatabaseConf(type: AdDbType): IRepoAd { + val dbSettingPath = "${ConfigPaths.repository}.${type.confName}" + val dbSetting = environment.config.propertyOrNull(dbSettingPath)?.getString()?.lowercase() + return when (dbSetting) { + "in-memory", "inmemory", "memory", "mem" -> initInMemory() +// "postgres", "postgresql", "pg", "sql", "psql" -> initPostgres() + else -> throw IllegalArgumentException( + "$dbSettingPath must be set in application.yml to one of: " + + "'inmemory', 'postgres', 'cassandra', 'gremlin'" + ) + } +} diff --git a/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/configs/ConfigPaths.kt b/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/configs/ConfigPaths.kt new file mode 100644 index 0000000..1b23471 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/configs/ConfigPaths.kt @@ -0,0 +1,6 @@ +package ru.otus.otuskotlin.marketplace.app.ktor.configs + +object ConfigPaths { + const val mkplRoot = "marketplace" + const val repository = "$mkplRoot.repository" +} diff --git a/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/configs/PostgresConfig.kt b/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/configs/PostgresConfig.kt new file mode 100644 index 0000000..9597774 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/configs/PostgresConfig.kt @@ -0,0 +1,25 @@ +package ru.otus.otuskotlin.marketplace.app.ktor.configs + +import io.ktor.server.config.* + +data class PostgresConfig( + val host: String = "localhost", + val port: Int = 5432, + val user: String = "postgres", + val password: String = "marketplace-pass", + val database: String = "marketplace-ads", + val schema: String = "public", +) { + constructor(config: ApplicationConfig): this( + host = config.propertyOrNull("$PATH.host")?.getString() ?: "localhost", + port = config.propertyOrNull("$PATH.port")?.getString()?.toIntOrNull() ?: 5432, + user = config.propertyOrNull("$PATH.user")?.getString() ?: "postgres", + password = config.property("$PATH.password").getString(), + database = config.propertyOrNull("$PATH.database")?.getString() ?: "marketplace-ads", + schema = config.propertyOrNull("$PATH.schema")?.getString() ?: "public", + ) + + companion object { + const val PATH = "${ConfigPaths.repository}.psql" + } +} diff --git a/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/plugins/InitAppSettings.kt b/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/plugins/InitAppSettings.kt index 10fd784..70f10be 100644 --- a/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/plugins/InitAppSettings.kt +++ b/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/plugins/InitAppSettings.kt @@ -6,14 +6,13 @@ import ru.otus.otuskotlin.marketplace.app.ktor.base.KtorWsSessionRepo import ru.otus.otuskotlin.marketplace.backend.repository.inmemory.AdRepoStub import ru.otus.otuskotlin.marketplace.biz.MkplAdProcessor import ru.otus.otuskotlin.marketplace.common.MkplCorSettings -import ru.otus.otuskotlin.marketplace.repo.inmemory.AdRepoInMemory fun Application.initAppSettings(): MkplAppSettings { val corSettings = MkplCorSettings( loggerProvider = getLoggerProviderConf(), wsSessions = KtorWsSessionRepo(), - repoTest = AdRepoInMemory(), - repoProd = AdRepoInMemory(), + repoTest = getDatabaseConf(AdDbType.TEST), + repoProd = getDatabaseConf(AdDbType.PROD), repoStub = AdRepoStub(), ) return MkplAppSettings( diff --git a/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/plugins/getDatabaseConf.kt b/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/plugins/getDatabaseConf.kt new file mode 100644 index 0000000..d20252b --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/kotlin/plugins/getDatabaseConf.kt @@ -0,0 +1,38 @@ +package ru.otus.otuskotlin.marketplace.app.ktor.plugins + +import io.ktor.server.application.* +import ru.otus.otuskotlin.marketplace.app.ktor.configs.PostgresConfig +import ru.otus.otuskotlin.marketplace.backend.repo.postgresql.RepoAdSql +import ru.otus.otuskotlin.marketplace.backend.repo.postgresql.SqlProperties +import ru.otus.otuskotlin.marketplace.common.repo.IRepoAd +import ru.otus.otuskotlin.marketplace.repo.inmemory.AdRepoInMemory +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes + +expect fun Application.getDatabaseConf(type: AdDbType): IRepoAd + +enum class AdDbType(val confName: String) { + PROD("prod"), TEST("test") +} + +fun Application.initInMemory(): IRepoAd { + val ttlSetting = environment.config.propertyOrNull("db.prod")?.getString()?.let { + Duration.parse(it) + } + return AdRepoInMemory(ttl = ttlSetting ?: 10.minutes) +} + + +fun Application.initPostgres(): IRepoAd { + val config = PostgresConfig(environment.config) + return RepoAdSql( + properties = SqlProperties( + host = config.host, + port = config.port, + user = config.user, + password = config.password, + schema = config.schema, + database = config.database, + ) + ) +} diff --git a/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/resources/application.yaml b/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/resources/application.yaml index 5ad7e6a..c80c65c 100644 --- a/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/resources/application.yaml +++ b/ok-marketplace-be/ok-marketplace-app-ktor/src/commonMain/resources/application.yaml @@ -24,3 +24,15 @@ ktor: # logger: socket # socketLogger: # port: 24224 + +marketplace: + repository: + test: "inmemory" + prod: "$DB_TYPE_PROD:inmemory" + psql: + schema: public + database: "$MKPLADS_DB:marketplace-ads" + host: "$MKPLADS_HOST:localhost" + port: "$MKPLADS_PORT:5432" + user: "$MKPLADS_USER:postgres" + password: "$MKPLADS_PASS:marketplace-pass" diff --git a/ok-marketplace-be/ok-marketplace-app-ktor/src/jvmMain/kotlin/plugins/getDatabaseConf.jvm.kt b/ok-marketplace-be/ok-marketplace-app-ktor/src/jvmMain/kotlin/plugins/getDatabaseConf.jvm.kt new file mode 100644 index 0000000..4196e82 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-app-ktor/src/jvmMain/kotlin/plugins/getDatabaseConf.jvm.kt @@ -0,0 +1,18 @@ +package ru.otus.otuskotlin.marketplace.app.ktor.plugins + +import io.ktor.server.application.* +import ru.otus.otuskotlin.marketplace.app.ktor.configs.ConfigPaths +import ru.otus.otuskotlin.marketplace.common.repo.IRepoAd + +actual fun Application.getDatabaseConf(type: AdDbType): IRepoAd { + val dbSettingPath = "${ConfigPaths.repository}.${type.confName}" + val dbSetting = environment.config.propertyOrNull(dbSettingPath)?.getString()?.lowercase() + return when (dbSetting) { + "in-memory", "inmemory", "memory", "mem" -> initInMemory() + "postgres", "postgresql", "pg", "sql", "psql" -> initPostgres() + else -> throw IllegalArgumentException( + "$dbSettingPath must be set in application.yml to one of: " + + "'inmemory', 'postgres', 'cassandra', 'gremlin'" + ) + } +} diff --git a/ok-marketplace-be/ok-marketplace-app-ktor/src/jvmMain/resources/application.yaml b/ok-marketplace-be/ok-marketplace-app-ktor/src/jvmMain/resources/application.yaml index 053d208..2de9178 100644 --- a/ok-marketplace-be/ok-marketplace-app-ktor/src/jvmMain/resources/application.yaml +++ b/ok-marketplace-be/ok-marketplace-app-ktor/src/jvmMain/resources/application.yaml @@ -40,3 +40,14 @@ ktor: # queue: mkpl-ads-v1-queue # consumerTag: "mkpl-ads-v1-consumer" # exchangeType: direct +marketplace: + repository: + test: "inmemory" + prod: "$DB_TYPE_PROD:inmemory" + psql: + schema: public + database: "$MKPLADS_DB:marketplace-ads" + host: "$MKPLADS_HOST:localhost" + port: "$MKPLADS_PORT:5432" + user: "$MKPLADS_USER:postgres" + password: "$MKPLADS_PASS:marketplace-pass" diff --git a/ok-marketplace-be/ok-marketplace-app-ktor/src/linuxX64Main/kotlin/plugins/getDatabaseConf.linuxX64.kt b/ok-marketplace-be/ok-marketplace-app-ktor/src/linuxX64Main/kotlin/plugins/getDatabaseConf.linuxX64.kt new file mode 100644 index 0000000..4196e82 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-app-ktor/src/linuxX64Main/kotlin/plugins/getDatabaseConf.linuxX64.kt @@ -0,0 +1,18 @@ +package ru.otus.otuskotlin.marketplace.app.ktor.plugins + +import io.ktor.server.application.* +import ru.otus.otuskotlin.marketplace.app.ktor.configs.ConfigPaths +import ru.otus.otuskotlin.marketplace.common.repo.IRepoAd + +actual fun Application.getDatabaseConf(type: AdDbType): IRepoAd { + val dbSettingPath = "${ConfigPaths.repository}.${type.confName}" + val dbSetting = environment.config.propertyOrNull(dbSettingPath)?.getString()?.lowercase() + return when (dbSetting) { + "in-memory", "inmemory", "memory", "mem" -> initInMemory() + "postgres", "postgresql", "pg", "sql", "psql" -> initPostgres() + else -> throw IllegalArgumentException( + "$dbSettingPath must be set in application.yml to one of: " + + "'inmemory', 'postgres', 'cassandra', 'gremlin'" + ) + } +} diff --git a/ok-marketplace-be/ok-marketplace-app-spring/build.gradle.kts b/ok-marketplace-be/ok-marketplace-app-spring/build.gradle.kts index cfe0452..7f316a6 100644 --- a/ok-marketplace-be/ok-marketplace-app-spring/build.gradle.kts +++ b/ok-marketplace-be/ok-marketplace-app-spring/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { // DB implementation(projects.okMarketplaceRepoStubs) implementation(projects.okMarketplaceRepoInmemory) + implementation(projects.okMarketplaceRepoPostgres) testImplementation(projects.okMarketplaceRepoCommon) testImplementation(projects.okMarketplaceStubs) @@ -66,4 +67,5 @@ tasks { tasks.withType { useJUnitPlatform() + environment("MKPLADS_DB", "test_db") } diff --git a/ok-marketplace-be/ok-marketplace-app-spring/src/main/kotlin/config/AdConfig.kt b/ok-marketplace-be/ok-marketplace-app-spring/src/main/kotlin/config/AdConfig.kt index a3ef2dc..3e18e56 100644 --- a/ok-marketplace-be/ok-marketplace-app-spring/src/main/kotlin/config/AdConfig.kt +++ b/ok-marketplace-be/ok-marketplace-app-spring/src/main/kotlin/config/AdConfig.kt @@ -1,9 +1,13 @@ package ru.otus.otuskotlin.markeplace.app.spring.config +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import ru.otus.otuskotlin.markeplace.app.spring.base.MkplAppSettings import ru.otus.otuskotlin.markeplace.app.spring.base.SpringWsSessionRepo +import ru.otus.otuskotlin.marketplace.backend.repo.postgresql.RepoAdSql import ru.otus.otuskotlin.marketplace.backend.repository.inmemory.AdRepoStub import ru.otus.otuskotlin.marketplace.biz.MkplAdProcessor import ru.otus.otuskotlin.marketplace.common.MkplCorSettings @@ -13,8 +17,12 @@ import ru.otus.otuskotlin.marketplace.logging.jvm.mpLoggerLogback import ru.otus.otuskotlin.marketplace.repo.inmemory.AdRepoInMemory @Suppress("unused") +@EnableConfigurationProperties(AdConfigPostgres::class) @Configuration -class AdConfig { +class AdConfig(val postgresConfig: AdConfigPostgres) { + + val logger: Logger = LoggerFactory.getLogger(AdConfig::class.java) + @Bean fun processor(corSettings: MkplCorSettings) = MkplAdProcessor(corSettings = corSettings) @@ -25,7 +33,9 @@ class AdConfig { fun testRepo(): IRepoAd = AdRepoInMemory() @Bean - fun prodRepo(): IRepoAd = AdRepoInMemory() + fun prodRepo(): IRepoAd = RepoAdSql(postgresConfig.psql).apply { + logger.info("Connecting to DB with ${this}") + } @Bean fun stubRepo(): IRepoAd = AdRepoStub() diff --git a/ok-marketplace-be/ok-marketplace-app-spring/src/main/kotlin/config/AdConfigPostgres.kt b/ok-marketplace-be/ok-marketplace-app-spring/src/main/kotlin/config/AdConfigPostgres.kt new file mode 100644 index 0000000..7758500 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-app-spring/src/main/kotlin/config/AdConfigPostgres.kt @@ -0,0 +1,27 @@ +package ru.otus.otuskotlin.markeplace.app.spring.config + +import org.springframework.boot.context.properties.ConfigurationProperties +import ru.otus.otuskotlin.marketplace.backend.repo.postgresql.SqlProperties + +// Так не работает +//@ConfigurationProperties(prefix = "markeplace.repository.psql") +@ConfigurationProperties(prefix = "psql") +data class AdConfigPostgres( + var host: String = "localhost", + var port: Int = 5432, + var user: String = "postgres", + var password: String = "marketplace-pass", + var database: String = "marketplace_ads", + var schema: String = "public", + var table: String = "ads", +) { + val psql: SqlProperties = SqlProperties( + host = host, + port = port, + user = user, + password = password, + database = database, + schema = schema, + table = table, + ) +} diff --git a/ok-marketplace-be/ok-marketplace-app-spring/src/main/resources/application.yml b/ok-marketplace-be/ok-marketplace-app-spring/src/main/resources/application.yml index d4aa02c..0467568 100644 --- a/ok-marketplace-be/ok-marketplace-app-spring/src/main/resources/application.yml +++ b/ok-marketplace-be/ok-marketplace-app-spring/src/main/resources/application.yml @@ -24,3 +24,16 @@ springdoc: url: specs-ad-v1.yaml - name: v2 url: specs-ad-v2.yaml + +marketplace: + repository: + test: "inmemory" + prod: "$DB_TYPE_PROD:inmemory" + +psql: + schema: public + database: "${MKPLADS_DB:marketplace-ads}" + host: "${MKPLADS_HOST:localhost}" + port: "${MKPLADS_PORT:5433}" + user: "${MKPLADS_USER:postgres}" + password: "${MKPLADS_PASS:marketplace-pass}" diff --git a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/ApplicationTests.kt b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/ApplicationTests.kt index 137e290..7455b2f 100644 --- a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/ApplicationTests.kt +++ b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/ApplicationTests.kt @@ -1,12 +1,19 @@ package ru.otus.otuskotlin.markeplace.app.spring +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import ru.otus.otuskotlin.markeplace.app.spring.config.AdConfigPostgres @SpringBootTest class ApplicationTests { + @Autowired + var pgConf: AdConfigPostgres = AdConfigPostgres() @Test fun contextLoads() { + assertEquals(5433, pgConf.psql.port) + assertEquals("test_db", pgConf.psql.database) } } diff --git a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoBaseV1Test.kt b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoBaseV1Test.kt index 7cf4d64..adbe5eb 100644 --- a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoBaseV1Test.kt +++ b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoBaseV1Test.kt @@ -25,8 +25,7 @@ internal abstract class AdRepoBaseV1Test { ), prepareCtx(MkplAdStub.prepareResult { id = MkplAdId(uuidNew) - ownerId = MkplUserId.NONE - lock = MkplAdLock.NONE + lock = MkplAdLock(uuidNew) }) .toTransportCreate() .copy(responseType = "create") @@ -51,7 +50,7 @@ internal abstract class AdRepoBaseV1Test { ad = MkplAdStub.prepareResult { title = "add" }.toTransportUpdate(), debug = debug, ), - prepareCtx(MkplAdStub.prepareResult { title = "add" }) + prepareCtx(MkplAdStub.prepareResult { title = "add"; lock = MkplAdLock(uuidNew) }) .toTransportUpdate().copy(responseType = "update") ) @@ -93,7 +92,7 @@ internal abstract class AdRepoBaseV1Test { ), MkplContext( state = MkplState.RUNNING, - adResponse = MkplAdStub.get(), + adResponse = MkplAdStub.prepareResult { permissionsClient.clear() }, adsResponse = MkplAdStub.prepareSearchList("xx", MkplDealSide.SUPPLY) .onEach { it.permissionsClient.clear() } .sortedBy { it.id.asString() } diff --git a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoBaseV2Test.kt b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoBaseV2Test.kt index da7fc8a..b1d67d4 100644 --- a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoBaseV2Test.kt +++ b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoBaseV2Test.kt @@ -25,7 +25,6 @@ internal abstract class AdRepoBaseV2Test { ), prepareCtx(MkplAdStub.prepareResult { id = MkplAdId(uuidNew) - ownerId = MkplUserId.NONE lock = MkplAdLock(uuidNew) }) .toTransportCreate() @@ -93,7 +92,7 @@ internal abstract class AdRepoBaseV2Test { ), MkplContext( state = MkplState.RUNNING, - adResponse = MkplAdStub.get(), + adResponse = MkplAdStub.prepareResult { permissionsClient.clear() }, adsResponse = MkplAdStub.prepareSearchList("xx", MkplDealSide.SUPPLY) .onEach { it.permissionsClient.clear() } .sortedBy { it.id.asString() } diff --git a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoInMemoryV1Test.kt b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoInMemoryV1Test.kt index 49fbb84..ce40dc3 100644 --- a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoInMemoryV1Test.kt +++ b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoInMemoryV1Test.kt @@ -7,6 +7,7 @@ import org.junit.jupiter.api.BeforeEach import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Qualifier import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest +import org.springframework.context.annotation.Import import org.springframework.test.web.reactive.server.WebTestClient import ru.otus.otuskotlin.markeplace.app.spring.config.AdConfig import ru.otus.otuskotlin.markeplace.app.spring.controllers.AdControllerV1Fine @@ -21,8 +22,13 @@ import ru.otus.otuskotlin.marketplace.stubs.MkplAdStub import kotlin.test.Test // Temporary simple test with stubs -@WebFluxTest(AdControllerV1Fine::class, AdConfig::class) +@WebFluxTest( + AdControllerV1Fine::class, AdConfig::class, + properties = ["spring.main.allow-bean-definition-overriding=true"] +) +@Import(RepoInMemoryConfig::class) internal class AdRepoInMemoryV1Test : AdRepoBaseV1Test() { + @Autowired override lateinit var webClient: WebTestClient diff --git a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoInMemoryV2Test.kt b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoInMemoryV2Test.kt index f678f26..a81b6d9 100644 --- a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoInMemoryV2Test.kt +++ b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/AdRepoInMemoryV2Test.kt @@ -7,6 +7,7 @@ import org.junit.jupiter.api.BeforeEach import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Qualifier import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest +import org.springframework.context.annotation.Import import org.springframework.test.web.reactive.server.WebTestClient import ru.otus.otuskotlin.markeplace.app.spring.config.AdConfig import ru.otus.otuskotlin.markeplace.app.spring.controllers.AdControllerV2Fine @@ -21,7 +22,11 @@ import ru.otus.otuskotlin.marketplace.stubs.MkplAdStub import kotlin.test.Test // Temporary simple test with stubs -@WebFluxTest(AdControllerV2Fine::class, AdConfig::class) +@WebFluxTest( + AdControllerV2Fine::class, AdConfig::class, + properties = ["spring.main.allow-bean-definition-overriding=true"] +) +@Import(RepoInMemoryConfig::class) internal class AdRepoInMemoryV2Test : AdRepoBaseV2Test() { @Autowired override lateinit var webClient: WebTestClient diff --git a/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/RepoInMemoryConfig.kt b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/RepoInMemoryConfig.kt new file mode 100644 index 0000000..0c930d0 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-app-spring/src/test/kotlin/repo/RepoInMemoryConfig.kt @@ -0,0 +1,15 @@ +package ru.otus.otuskotlin.markeplace.app.spring.repo + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import ru.otus.otuskotlin.marketplace.common.repo.IRepoAd +import ru.otus.otuskotlin.marketplace.repo.inmemory.AdRepoInMemory + +@TestConfiguration +class RepoInMemoryConfig { + @Suppress("unused") + @Bean() + @Primary + fun prodRepo(): IRepoAd = AdRepoInMemory() +} diff --git a/ok-marketplace-be/ok-marketplace-app-spring/src/test/resources/application.yml b/ok-marketplace-be/ok-marketplace-app-spring/src/test/resources/application.yml index d4aa02c..48f0bf9 100644 --- a/ok-marketplace-be/ok-marketplace-app-spring/src/test/resources/application.yml +++ b/ok-marketplace-be/ok-marketplace-app-spring/src/test/resources/application.yml @@ -24,3 +24,16 @@ springdoc: url: specs-ad-v1.yaml - name: v2 url: specs-ad-v2.yaml + +marketplace: + repository: + test: "inmemory" + prod: "$DB_TYPE_PROD:inmemory" + +psql: + schema: public + database: "${MKPLADS_DB:marketplace-ads}" + host: "${MKPLADS_HOST:localhost}" + port: 5433 #"${MKPLADS_PORT:5433}" + user: "${MKPLADS_USER:postgres}" + password: "${MKPLADS_PASS:marketplace-pass}" diff --git a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/repo/AdRepoPrepareCreate.kt b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/repo/AdRepoPrepareCreate.kt index f70391f..7a18cbf 100644 --- a/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/repo/AdRepoPrepareCreate.kt +++ b/ok-marketplace-be/ok-marketplace-biz/src/commonMain/kotlin/repo/AdRepoPrepareCreate.kt @@ -2,9 +2,9 @@ package ru.otus.otuskotlin.marketplace.biz.repo import ru.otus.otuskotlin.marketplace.common.MkplContext import ru.otus.otuskotlin.marketplace.common.models.MkplState -import ru.otus.otuskotlin.marketplace.common.models.MkplUserId import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl import ru.otus.otuskotlin.marketplace.cor.worker +import ru.otus.otuskotlin.marketplace.stubs.MkplAdStub fun ICorChainDsl.repoPrepareCreate(title: String) = worker { this.title = title @@ -13,6 +13,6 @@ fun ICorChainDsl.repoPrepareCreate(title: String) = worker { handle { adRepoPrepare = adValidated.deepCopy() // TODO будет реализовано в занятии по управлению пользвателями - adRepoPrepare.ownerId = MkplUserId.NONE + adRepoPrepare.ownerId = MkplAdStub.get().ownerId } } diff --git a/ok-marketplace-be/ok-marketplace-repo-common/src/commonMain/kotlin/AdRepoInitialized.kt b/ok-marketplace-be/ok-marketplace-repo-common/src/commonMain/kotlin/AdRepoInitialized.kt index c783569..f9719d3 100644 --- a/ok-marketplace-be/ok-marketplace-repo-common/src/commonMain/kotlin/AdRepoInitialized.kt +++ b/ok-marketplace-be/ok-marketplace-repo-common/src/commonMain/kotlin/AdRepoInitialized.kt @@ -6,7 +6,7 @@ import ru.otus.otuskotlin.marketplace.common.models.MkplAd * Делегат для всех репозиториев, позволяющий инициализировать базу данных предзагруженными данными */ class AdRepoInitialized( - private val repo: IRepoAdInitializable, + val repo: IRepoAdInitializable, initObjects: Collection = emptyList(), ) : IRepoAdInitializable by repo { @Suppress("unused") diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/build.gradle.kts b/ok-marketplace-be/ok-marketplace-repo-postgres/build.gradle.kts new file mode 100644 index 0000000..64227f1 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/build.gradle.kts @@ -0,0 +1,175 @@ +import com.bmuschko.gradle.docker.tasks.container.* +import com.bmuschko.gradle.docker.tasks.image.DockerPullImage +import com.github.dockerjava.api.command.InspectContainerResponse +import com.github.dockerjava.api.model.ExposedPort +import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest +import java.util.concurrent.atomic.AtomicBoolean + +plugins { + id("build-kmp") +// id("build-pgContainer") + alias(libs.plugins.muschko.remote) + alias(libs.plugins.liquibase) +} +repositories { + google() + mavenCentral() +} + +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(projects.okMarketplaceCommon) + api(projects.okMarketplaceRepoCommon) + + implementation(libs.coroutines.core) + implementation(libs.uuid) + } + } + commonTest { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + implementation(projects.okMarketplaceRepoTests) + } + } + jvmMain { + dependencies { + implementation(kotlin("stdlib-jdk8")) + implementation(libs.db.postgres) +// implementation(libs.db.hikari) + implementation(libs.bundles.exposed) + } + } + jvmTest { + dependencies { + implementation(kotlin("test-junit")) + } + } + nativeMain { + dependencies { + implementation(kotlin("stdlib")) + } + } + linuxX64Main { + dependencies { + implementation(kotlin("stdlib")) + implementation("io.github.moreirasantos:pgkn:1.1.0") + } + } + } +} + +dependencies { + liquibaseRuntime(libs.liquibase.core) + liquibaseRuntime(libs.liquibase.picocli) + liquibaseRuntime(libs.liquibase.snakeyml) + liquibaseRuntime(libs.db.postgres) +} + +var pgPort = 5432 +val taskGroup = "pgContainer" +val pgDbName = "marketplace_ads" +val pgUsername = "postgres" +val pgPassword = "marketplace-pass" +val containerStarted = AtomicBoolean(false) + +tasks { + // Здесь в тасках запускаем PotgreSQL в контейнере + // Накатываем liquibase миграцию + // Передаем настройки в среду тестирования + val postgresImage = "postgres:latest" + val pullImage by creating(DockerPullImage::class) { + group = taskGroup + image.set(postgresImage) + } + val dbContainer by creating(DockerCreateContainer::class) { + group = taskGroup + dependsOn(pullImage) + targetImageId(pullImage.image) + withEnvVar("POSTGRES_PASSWORD", pgPassword) + withEnvVar("POSTGRES_USER", pgUsername) + withEnvVar("POSTGRES_DB", pgDbName) + healthCheck.cmd("pg_isready") + hostConfig.portBindings.set(listOf(":5432")) + exposePorts("tcp", listOf(5432)) + hostConfig.autoRemove.set(true) + } + val stopPg by creating(DockerStopContainer::class) { + group = taskGroup + targetContainerId(dbContainer.containerId) + } + val startPg by creating(DockerStartContainer::class) { + group = taskGroup + dependsOn(dbContainer) + targetContainerId(dbContainer.containerId) + finalizedBy(stopPg) + } + val inspectPg by creating(DockerInspectContainer::class) { + group = taskGroup + dependsOn(startPg) + finalizedBy(stopPg) + targetContainerId(dbContainer.containerId) + onNext( + object : Action { + override fun execute(container: InspectContainerResponse) { + pgPort = container.networkSettings.ports.bindings[ExposedPort.tcp(5432)] + ?.first() + ?.hostPortSpec + ?.toIntOrNull() + ?: throw Exception("Postgres port is not found in container") + } + } + ) + } + val liquibaseUpdate = getByName("update") { + group = taskGroup + dependsOn(inspectPg) + finalizedBy(stopPg) + doFirst { + println("waiting for a while ${System.currentTimeMillis()/1000000}") + Thread.sleep(30000) + println("LQB: \"jdbc:postgresql://localhost:$pgPort/$pgDbName\" ${System.currentTimeMillis()/1000000}") + liquibase { + activities { + register("main") { + arguments = mapOf( + "logLevel" to "info", + "searchPath" to layout.projectDirectory.dir("migrations").asFile.toString(), + "changelogFile" to "changelog-v0.0.1.sql", + "url" to "jdbc:postgresql://localhost:$pgPort/$pgDbName", + "username" to pgUsername, + "password" to pgPassword, + "driver" to "org.postgresql.Driver" + ) + } + } + } + } + } + val waitPg by creating(DockerWaitContainer::class) { + group = taskGroup + dependsOn(inspectPg) + dependsOn(liquibaseUpdate) + containerId.set(startPg.containerId) + finalizedBy(stopPg) + doFirst { + println("PORT: $pgPort") + } + } + withType(KotlinNativeTest::class).configureEach { + dependsOn(liquibaseUpdate) + finalizedBy(stopPg) + doFirst { + environment("postgresPort", pgPort.toString()) + } + } + withType(Test::class).configureEach { + dependsOn(liquibaseUpdate) + finalizedBy(stopPg) + doFirst { + environment("postgresPort", pgPort.toString()) + } + } +} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/changelog-v0.0.1.sql b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/changelog-v0.0.1.sql new file mode 100644 index 0000000..dc1db71 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/changelog-v0.0.1.sql @@ -0,0 +1,24 @@ +--liquibase formatted sql + +--changeset sokatov:1 labels:v0.0.1 +CREATE TYPE "ad_types_type" AS ENUM ('demand', 'supply'); +CREATE TYPE "ad_visibilities_type" AS ENUM ('public', 'owner', 'group'); + +CREATE TABLE "ads" ( + "id" text primary key constraint ads_id_length_ctr check (length("id") < 64), + "title" text constraint ads_title_length_ctr check (length(title) < 128), + "description" text constraint ads_description_length_ctr check (length(title) < 4096), + "ad_type" ad_types_type not null, + "visibility" ad_visibilities_type not null, + "owner_id" text not null constraint ads_owner_id_length_ctr check (length(id) < 64), + "product_id" text constraint ads_product_id_length_ctr check (length(id) < 64), + "lock" text not null constraint ads_lock_length_ctr check (length(id) < 64) +); + +CREATE INDEX ads_owner_id_idx on "ads" using hash ("owner_id"); + +CREATE INDEX ads_product_id_idx on "ads" using hash ("product_id"); + +CREATE INDEX ads_ad_type_idx on "ads" using hash ("ad_type"); + +CREATE INDEX ads_visibility_idx on "ads" using hash ("visibility"); diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.advanced.flowfile.yaml b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.advanced.flowfile.yaml new file mode 100644 index 0000000..840defe --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.advanced.flowfile.yaml @@ -0,0 +1,147 @@ +########## LIQUIBASE FLOW FILE ########## +########## learn more http://docs.liquibase.com/flow ########## + +## NOTE: This is an advanced example flowfile, compared to the other sample at examples/liquibase.flowfile.yaml +#### HOW TO USE THIS FILE: +#### example for CLI: liquibase flow --flow-file=liquibase.advanced.flowfile.yaml +#### example for ENV Var: LIQUIBASE_FLOW_FLOW_FILE=liquibase.advanced.flowfile.yaml + +## Advanced options show in this file include: +#### non-default name of 'liquibase.advanced.flowfile.yaml' (use by setting flowfile property to this name) +#### use of 'include' to inject namespaced yaml files of key: val variables +#### use of globalVariables and stageVariables +#### use of globalArgs and cmdArgs +#### use of property substitution +#### use of a nested flowfile (in this case in the endStage, but could be elsewhere) +#### use of if: conditional which allows a -type: shell or -type: liquibase command to run +###### In the example below, we set an environment variable LIQUIBASE_CURRENT_TARGET, such as 'export LIQUIBASE_CURRENT_TARGET=dev' +###### This could be determined dynamically, of course, from the build tools, bu tthis is simpler for this example "if:" conditional +#### use of shell commands in a -type: shell block. +###### command: bash -c "the shell command || and its chained commands && go in the quotes" +######## +#### POTENTIAL use of environment variables: +###### DATETIME STAMP +######## In this file, you could replace ${FLOWVARS.THISDATE} with an env var, such as ${LIQUIBASE_THISDATE} set via .bash_profile +######## for example 'export LIQUIBASE_THISDATE=$( date +'%Y-%m-%dT%H-%M-%S' )' + + +## Bring in and namespace an external file with yaml 'key: val' pairs for use in this file +## The variables will be used as ${namespace.variablename}, seen in this example as ${FLOWVARS.PROJNAME} +include: + FLOWVARS: liquibase.flowvariables.yaml + + + +## Set up some global variables for property substitution in ANY stage +globalVariables: + DIRNAME: "./${FLOWVARS.PROJNAME}_${FLOWVARS.THISDATE}" + STATUSFILE: "status.txt" + UPDATELOG: "update.log" + HISTORYFILE: "history.txt" + + +## Start of the stages. +stages: + + ## A prep stage. There can be more than one stage if desired. + stage-prep: + + actions: + + - type: shell + command: bash -c "mkdir -p ${DIRNAME}" + + + ## Another stage. + stage-dowork: + + ## set up vars for property substitution in THIS stage only + stageVariables: + + VERBOSESTATE: TRUE + + + actions: + # + # Do a validate command + # + - type: liquibase + command: validate + + # + # Tell me what is pending a deployment + # + - type: shell + command: bash -c "liquibase --show-banner false --outputfile ./${DIRNAME}/${STATUSFILE} status --verbose ${VERBOSESTATE}" + + # This is the structured way to setup a liquibase command, if you dont want to run it as one 'bash -c' command + #- type: liquibase + # command: status + # globalArgs: + # outputfile: "${DIRNAME}/${STATUSFILE}" + # showbanner: false + # cmdArgs: {verbose: "${VERBOSESTATE}" + + + + + # + # And then save a version in detail, if env var LIQUIBASE_FILE_OUTPUT == 1 + # + - type: shell + command: bash -c "echo 'LIQUIBASE_ env vars ' && env | grep 'LIQUIBASE_' " + + - type: liquibase + ## if this var LIQUIBASE_CURRENT_TARGET is "dev", then the updatesql will run + if: "${LIQUIBASE_CURRENT_TARGET} == dev" + command: updatesql + globalArgs: {outputfile: "${DIRNAME}/${UPDATELOG}"} + + - type: shell + ## if this var LIQUIBASE_CURRENT_TARGET is not "dev", then the message will be displayed + if: "${LIQUIBASE_CURRENT_TARGET} != dev" + command: echo "No output files created. Set env var LIQUIBASE_CURRENT_TARGET to dev to trigger file creation." + + + + # + # Quality Checks for changelog + # + - type: liquibase + command: checks run + cmdArgs: {checks-scope: changelog} + + # + # Run update + # + - type: liquibase + command: update + + + # + # Quality Checks for database + # + - type: liquibase + command: checks run + cmdArgs: {checks-scope: database} + + + # + # Create a history file + # + - type: liquibase + command: history + globalArgs: {outputfile: "${DIRNAME}/${HISTORYFILE}"} + + + +## The endStage ALWAYS RUNS. +## So put actions here which you desire to perform whether previous stages' actions succeed or fail. +## If you do not want any actions to ALWAYS RUN, simply delete the endStage from your flow file. + +endStage: + actions: + - type: liquibase + ## Notice this is a flow command in a flow file, and it called a 'nested' flowfile, which in this case lives in the same dir, but could be elsewhere + command: flow + cmdArgs: {flowfile: liquibase.endstage.flow} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.checks-package.yaml b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.checks-package.yaml new file mode 100644 index 0000000..887d0b8 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.checks-package.yaml @@ -0,0 +1,14 @@ +## This is a Liquibase checks packages formatted yaml file. +## It contains just one or more 'checksPackages' objects +## with one or more named checks-settings-files. +## These checks-settings-files listed in a package are processed sequentially +## during 'liquibase checks run' or 'liquibase checks show' commands +## which specify this file as the checks-settings-file property. +## Learn more at https://docs.liquibase.com/quality-checks + +## Uncomment and change names with your checks-settings-files +#checksPackages: +# - name: "my-checks-files.pkg" +# files: +# - "./liquibase.checks-settings.conf" +# - "./liquibase.more.checks-settings.yaml" \ No newline at end of file diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.endstage.flow b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.endstage.flow new file mode 100644 index 0000000..f44af55 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.endstage.flow @@ -0,0 +1,31 @@ +########## LIQUIBASE FLOW FILE ########## +########## learn more http://docs.liquibase.com/flow ########## + +## NOTE: This example flowfile is called from the examples/liquibase.advanced.flowfile.yaml file +## While it could be run on its own, this file is designed to show that flow-files can be decomposed +## into separate files as makes sense for your use cases. + +stages: + + cleanuptheDB: + + actions: + + # + # Clear out the database + # + - type: liquibase + command: dropAll + + # + # Check that database is empty by seeing what is ready to be deployed + # + - type: liquibase + command: status + cmdArgs: {verbose: TRUE} + + +## The endStage ALWAYS RUNS. +## So put actions here which you desire to perform whether previous stages' actions succeed or fail. +## If you do not want any actions to ALWAYS RUN, simply delete the endStage from your flow file, +## as it has been deleted here in this liquibase.endStage.flow file. diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.flowfile.yaml b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.flowfile.yaml new file mode 100644 index 0000000..58419bb --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.flowfile.yaml @@ -0,0 +1,41 @@ +########## LIQUIBASE FLOWFILE ########## +########## learn more http://docs.liquibase.com/flow ########## + +## Note: Any command which fails in any stage below result in the command stopping, and endStage being run. +## A flow file can have one or more stages, each with multiple "actions", +## or your flow file can have multiple stages with fewer actions in each stage. +stages: + + + ## The first stage of actions. + Default: + + actions: + # + # Quality Checks for changelog + # + - type: liquibase + command: checks run + cmdArgs: {checks-scope: changelog} + # + # Run the update + # + - type: liquibase + command: update + + # + # Quality Checks for database + # + - type: liquibase + command: checks run + cmdArgs: {checks-scope: database} + + +## The endStage ALWAYS RUNS. +## So put actions here which you desire to perform whether previous stages' actions succeed or fail. +## If you do not want any actions to ALWAYS RUN, simply delete the endStage from your flow file. + +endStage: + actions: + - type: liquibase + command: history diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.flowvariables.yaml b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.flowvariables.yaml new file mode 100644 index 0000000..7b41bb1 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.flowvariables.yaml @@ -0,0 +1,8 @@ +########## LIQUIBASE FLOWFILE ########## +########## learn more http://docs.liquibase.com/flow ########## + +## NOTE: This example yaml file of key:value variables is injected into examples/liquibase.advanced.flow file +## using the "include" ability. It will be given the namespace "DATES" but could be given any namespace. + +PROJNAME: "MyFlowProj" +THISDATE: "2022-11-28T15-00-20" \ No newline at end of file diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.properties b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.properties new file mode 100644 index 0000000..b1687f8 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/migrations/liquibase.properties @@ -0,0 +1,64 @@ +#### _ _ _ _ +## | | (_) (_) | +## | | _ __ _ _ _ _| |__ __ _ ___ ___ +## | | | |/ _` | | | | | '_ \ / _` / __|/ _ \ +## | |___| | (_| | |_| | | |_) | (_| \__ \ __/ +## \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___| +## | | +## |_| +## +## The liquibase.properties file stores properties which do not change often, +## such as database connection information. Properties stored here save time +## and reduce risk of mistyped command line arguments. +## Learn more: https://docs.liquibase.com/concepts/connections/creating-config-properties.html +#### +#### +## Note about relative and absolute paths: +## The liquibase.properties file requires paths for some properties. +## The classpath is the path/to/resources (ex. src/main/resources). +## The changeLogFile path is relative to the classpath. +## The url H2 example below is relative to 'pwd' resource. +#### +# Enter the path for your changelog file. +changeLogFile=changelog-v0.0.1.sql + +#### Enter the Target database 'url' information #### +#liquibase.command.url=jdbc:postgresql://localhost:5432/marketplace_ads +liquibase.command.url=jdbc:postgresql://localhost:32794/marketplace_ads + +# Enter the username for your Target database. +liquibase.command.username: postgres + +# Enter the password for your Target database. +liquibase.command.password: marketplace-pass + +#### Enter the Source Database 'referenceUrl' information #### +## The source database is the baseline or reference against which your target database is compared for diff/diffchangelog commands. + +# Enter URL for the source database +liquibase.command.referenceUrl: jdbc:postgresql://localhost:5432/marketplace_ads + +# Enter the username for your source database +liquibase.command.referenceUsername: postgres + +# Enter the password for your source database +liquibase.command.referencePassword: marketplace-pass + +# Logging Configuration +# logLevel controls the amount of logging information generated. If not set, the default logLevel is INFO. +# Valid values, from least amount of logging to most, are: +# OFF, ERROR, WARN, INFO, DEBUG, TRACE, ALL +# If you are having problems, setting the logLevel to DEBUG and re-running the command can be helpful. +# logLevel: DEBUG + +# The logFile property controls where logging messages are sent. If this is not set, then logging messages are +# displayed on the console. If this is set, then messages will be sent to a file with the given name. +# logFile: liquibase.log + +#### Liquibase Pro Key Information #### +# Learn more, contact support, or get or renew a Pro Key at https://www.liquibase.com/trial +# liquibase.licenseKey: + +## Get documentation at docs.liquibase.com ## +## Get certified courses at learn.liquibase.com ## +## Get support at liquibase.com/support ## diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/appleMain/kotlin/RepoAdSql.apple.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/appleMain/kotlin/RepoAdSql.apple.kt new file mode 100644 index 0000000..4792d57 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/appleMain/kotlin/RepoAdSql.apple.kt @@ -0,0 +1,39 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import ru.otus.otuskotlin.marketplace.common.models.MkplAd +import ru.otus.otuskotlin.marketplace.common.repo.* +import ru.otus.otuskotlin.marketplace.repo.common.IRepoAdInitializable + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +actual class RepoAdSql actual constructor( + properties: SqlProperties, + randomUuid: () -> String +) : IRepoAd, IRepoAdInitializable { + actual override fun save(ads: Collection): Collection { + TODO("Not yet implemented") + } + + actual override suspend fun createAd(rq: DbAdRequest): IDbAdResponse { + TODO("Not yet implemented") + } + + actual override suspend fun readAd(rq: DbAdIdRequest): IDbAdResponse { + TODO("Not yet implemented") + } + + actual override suspend fun updateAd(rq: DbAdRequest): IDbAdResponse { + TODO("Not yet implemented") + } + + actual override suspend fun deleteAd(rq: DbAdIdRequest): IDbAdResponse { + TODO("Not yet implemented") + } + + actual override suspend fun searchAd(rq: DbAdFilterRequest): IDbAdsResponse { + TODO("Not yet implemented") + } + + actual fun clear() { + TODO("Not yet implemented") + } +} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/appleTest/kotlin/GetEnv.apple.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/appleTest/kotlin/GetEnv.apple.kt new file mode 100644 index 0000000..960dab1 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/appleTest/kotlin/GetEnv.apple.kt @@ -0,0 +1,5 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +actual fun getEnv(name: String): String? { + TODO("Not yet implemented") +} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonMain/kotlin/RepoAdSql.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonMain/kotlin/RepoAdSql.kt new file mode 100644 index 0000000..94da6e1 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonMain/kotlin/RepoAdSql.kt @@ -0,0 +1,20 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import com.benasher44.uuid.uuid4 +import ru.otus.otuskotlin.marketplace.common.models.MkplAd +import ru.otus.otuskotlin.marketplace.common.repo.* +import ru.otus.otuskotlin.marketplace.repo.common.IRepoAdInitializable + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +expect class RepoAdSql( + properties: SqlProperties, + randomUuid: () -> String = { uuid4().toString() }, +) : IRepoAd, IRepoAdInitializable { + override fun save(ads: Collection): Collection + override suspend fun createAd(rq: DbAdRequest): IDbAdResponse + override suspend fun readAd(rq: DbAdIdRequest): IDbAdResponse + override suspend fun updateAd(rq: DbAdRequest): IDbAdResponse + override suspend fun deleteAd(rq: DbAdIdRequest): IDbAdResponse + override suspend fun searchAd(rq: DbAdFilterRequest): IDbAdsResponse + fun clear() +} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonMain/kotlin/SqlFields.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonMain/kotlin/SqlFields.kt new file mode 100644 index 0000000..cbe5f35 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonMain/kotlin/SqlFields.kt @@ -0,0 +1,33 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +object SqlFields { + const val ID = "id" + const val TITLE = "title" + const val DESCRIPTION = "description" + const val AD_TYPE = "ad_type" + const val VISIBILITY = "visibility" + const val LOCK = "lock" + const val LOCK_OLD = "lock_old" + const val OWNER_ID = "owner_id" + const val PRODUCT_ID = "product_id" + + const val AD_TYPE_TYPE = "ad_types_type" + const val AD_TYPE_DEMAND = "demand" + const val AD_TYPE_SUPPLY = "supply" + + const val VISIBILITY_TYPE = "ad_visibilities_type" + const val VISIBILITY_PUBLIC = "public" + const val VISIBILITY_OWNER = "owner" + const val VISIBILITY_GROUP = "group" + + const val FILTER_TITLE = TITLE + const val FILTER_OWNER_ID = OWNER_ID + const val FILTER_AD_TYPE = AD_TYPE + + const val DELETE_OK = "DELETE_OK" + + fun String.quoted() = "\"$this\"" + val allFields = listOf( + ID, TITLE, DESCRIPTION, AD_TYPE, VISIBILITY, LOCK, OWNER_ID, PRODUCT_ID, + ) +} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonMain/kotlin/SqlProperties.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonMain/kotlin/SqlProperties.kt new file mode 100644 index 0000000..2d971aa --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonMain/kotlin/SqlProperties.kt @@ -0,0 +1,14 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +data class SqlProperties( + val host: String = "localhost", + val port: Int = 5432, + val user: String = "postgres", + val password: String = "marketplace-pass", + val database: String = "marketplace_ads", + val schema: String = "public", + val table: String = "ads", +) { + val url: String + get() = "jdbc:postgresql://${host}:${port}/${database}" +} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonTest/kotlin/GetEnv.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonTest/kotlin/GetEnv.kt new file mode 100644 index 0000000..5bef527 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonTest/kotlin/GetEnv.kt @@ -0,0 +1,3 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +expect fun getEnv(name: String): String? diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonTest/kotlin/RepoAdSQLTest.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonTest/kotlin/RepoAdSQLTest.kt new file mode 100644 index 0000000..2c129c2 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonTest/kotlin/RepoAdSQLTest.kt @@ -0,0 +1,50 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import ru.otus.otuskotlin.marketplace.backend.repo.tests.* +import ru.otus.otuskotlin.marketplace.common.repo.IRepoAd +import ru.otus.otuskotlin.marketplace.repo.common.AdRepoInitialized +import ru.otus.otuskotlin.marketplace.repo.common.IRepoAdInitializable +import kotlin.test.AfterTest + +private fun IRepoAd.clear() { + val pgRepo = (this as AdRepoInitialized).repo as RepoAdSql + pgRepo.clear() +} + +class RepoAdSQLCreateTest : RepoAdCreateTest() { + override val repo: IRepoAdInitializable = SqlTestCompanion.repoUnderTestContainer( + initObjects, + randomUuid = { uuidNew.asString() }, + ) + @AfterTest + fun tearDown() = repo.clear() +} + +class RepoAdSQLReadTest : RepoAdReadTest() { + override val repo: IRepoAd = SqlTestCompanion.repoUnderTestContainer(initObjects) + @AfterTest + fun tearDown() = repo.clear() +} + +class RepoAdSQLUpdateTest : RepoAdUpdateTest() { + override val repo: IRepoAd = SqlTestCompanion.repoUnderTestContainer( + initObjects, + randomUuid = { lockNew.asString() }, + ) + @AfterTest + fun tearDown() { + repo.clear() + } +} + +class RepoAdSQLDeleteTest : RepoAdDeleteTest() { + override val repo: IRepoAd = SqlTestCompanion.repoUnderTestContainer(initObjects) + @AfterTest + fun tearDown() = repo.clear() +} + +class RepoAdSQLSearchTest : RepoAdSearchTest() { + override val repo: IRepoAd = SqlTestCompanion.repoUnderTestContainer(initObjects) + @AfterTest + fun tearDown() = repo.clear() +} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonTest/kotlin/SqlTestCompanion.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonTest/kotlin/SqlTestCompanion.kt new file mode 100644 index 0000000..11a0539 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/commonTest/kotlin/SqlTestCompanion.kt @@ -0,0 +1,30 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import com.benasher44.uuid.uuid4 +import ru.otus.otuskotlin.marketplace.common.models.MkplAd +import ru.otus.otuskotlin.marketplace.repo.common.AdRepoInitialized +import ru.otus.otuskotlin.marketplace.repo.common.IRepoAdInitializable + +object SqlTestCompanion { + private const val HOST = "localhost" + private const val USER = "postgres" + private const val PASS = "marketplace-pass" + val PORT = getEnv("postgresPort")?.toIntOrNull() ?: 5432 + + fun repoUnderTestContainer( + initObjects: Collection = emptyList(), + randomUuid: () -> String = { uuid4().toString() }, + ): IRepoAdInitializable = AdRepoInitialized( + repo = RepoAdSql( + SqlProperties( + host = HOST, + user = USER, + password = PASS, + port = PORT, + ), + randomUuid = randomUuid + ), + initObjects = initObjects, + ) +} + diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmMain/kotlin/AdTable.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmMain/kotlin/AdTable.kt new file mode 100644 index 0000000..f4db899 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmMain/kotlin/AdTable.kt @@ -0,0 +1,43 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.statements.UpdateBuilder +import ru.otus.otuskotlin.marketplace.common.models.* + +class AdTable(tableName: String) : Table(tableName) { + val id = text(SqlFields.ID) + val title = text(SqlFields.TITLE).nullable() + val description = text(SqlFields.DESCRIPTION).nullable() + val owner = text(SqlFields.OWNER_ID) + val visibility = visibilityEnumeration(SqlFields.VISIBILITY) + val dealSide = adTypeEnumeration(SqlFields.AD_TYPE) + val lock = text(SqlFields.LOCK) + val productId = text(SqlFields.PRODUCT_ID).nullable() + + override val primaryKey = PrimaryKey(id) + + fun from(res: ResultRow) = MkplAd( + id = MkplAdId(res[id].toString()), + title = res[title] ?: "", + description = res[description] ?: "", + ownerId = MkplUserId(res[owner].toString()), + visibility = res[visibility], + adType = res[dealSide], + lock = MkplAdLock(res[lock]), + productId = res[productId]?.let { MkplProductId(it) } ?: MkplProductId.NONE, + ) + + fun to(it: UpdateBuilder<*>, ad: MkplAd, randomUuid: () -> String) { + it[id] = ad.id.takeIf { it != MkplAdId.NONE }?.asString() ?: randomUuid() + it[title] = ad.title + it[description] = ad.description + it[owner] = ad.ownerId.asString() + it[visibility] = ad.visibility + it[dealSide] = ad.adType + it[lock] = ad.lock.takeIf { it != MkplAdLock.NONE }?.asString() ?: randomUuid() + it[productId] = ad.productId.takeIf { it != MkplProductId.NONE }?.asString() + } + +} + diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmMain/kotlin/AdTypeEnumeration.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmMain/kotlin/AdTypeEnumeration.kt new file mode 100644 index 0000000..0b7853d --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmMain/kotlin/AdTypeEnumeration.kt @@ -0,0 +1,36 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import org.jetbrains.exposed.sql.Table +import org.postgresql.util.PGobject +import ru.otus.otuskotlin.marketplace.common.models.MkplDealSide + +fun Table.adTypeEnumeration( + columnName: String +) = customEnumeration( + name = columnName, + sql = SqlFields.AD_TYPE_TYPE, + fromDb = { value -> + when (value.toString()) { + SqlFields.AD_TYPE_DEMAND -> MkplDealSide.DEMAND + SqlFields.AD_TYPE_SUPPLY -> MkplDealSide.SUPPLY + else -> MkplDealSide.NONE + } + }, + toDb = { value -> + when (value) { + MkplDealSide.DEMAND -> PgAdTypeDemand + MkplDealSide.SUPPLY -> PgAdTypeSupply + MkplDealSide.NONE -> throw Exception("Wrong value of Ad Type. NONE is unsupported") + } + } +) + +sealed class PgAdTypeValue(enVal: String): PGobject() { + init { + type = SqlFields.AD_TYPE_TYPE + value = enVal + } +} + +object PgAdTypeDemand: PgAdTypeValue(SqlFields.AD_TYPE_DEMAND) +object PgAdTypeSupply: PgAdTypeValue(SqlFields.AD_TYPE_SUPPLY) diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmMain/kotlin/RepoAdSql.jvm.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmMain/kotlin/RepoAdSql.jvm.kt new file mode 100644 index 0000000..d6131e8 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmMain/kotlin/RepoAdSql.jvm.kt @@ -0,0 +1,126 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.transactions.transaction +import ru.otus.otuskotlin.marketplace.common.helpers.asMkplError +import ru.otus.otuskotlin.marketplace.common.models.* +import ru.otus.otuskotlin.marketplace.common.repo.* +import ru.otus.otuskotlin.marketplace.repo.common.IRepoAdInitializable + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +actual class RepoAdSql actual constructor( + properties: SqlProperties, + private val randomUuid: () -> String +) : IRepoAd, IRepoAdInitializable { + private val adTable = AdTable("${properties.schema}.${properties.table}") + + private val driver = when { + properties.url.startsWith("jdbc:postgresql://") -> "org.postgresql.Driver" + else -> throw IllegalArgumentException("Unknown driver for url ${properties.url}") + } + + private val conn = Database.connect( + properties.url, driver, properties.user, properties.password + ) + + actual fun clear(): Unit = transaction(conn) { + adTable.deleteAll() + } + + private fun saveObj(ad: MkplAd): MkplAd = transaction(conn) { + val res = adTable + .insert { + to(it, ad, randomUuid) + } + .resultedValues + ?.map { adTable.from(it) } + res?.first() ?: throw RuntimeException("BD error: insert statement returned empty result") + } + + private suspend inline fun transactionWrapper(crossinline block: () -> T, crossinline handle: (Exception) -> T): T = + withContext(Dispatchers.IO) { + try { + transaction(conn) { + block() + } + } catch (e: Exception) { + handle(e) + } + } + + private suspend inline fun transactionWrapper(crossinline block: () -> IDbAdResponse): IDbAdResponse = + transactionWrapper(block) { DbAdResponseErr(it.asMkplError()) } + + actual override fun save(ads: Collection): Collection = ads.map { saveObj(it) } + actual override suspend fun createAd(rq: DbAdRequest): IDbAdResponse = transactionWrapper { + DbAdResponseOk(saveObj(rq.ad)) + } + + private fun read(id: MkplAdId): IDbAdResponse { + val res = adTable.selectAll().where { + adTable.id eq id.asString() + }.singleOrNull() ?: return errorNotFound(id) + return DbAdResponseOk(adTable.from(res)) + } + + actual override suspend fun readAd(rq: DbAdIdRequest): IDbAdResponse = transactionWrapper { read(rq.id) } + + private suspend fun update( + id: MkplAdId, + lock: MkplAdLock, + block: (MkplAd) -> IDbAdResponse + ): IDbAdResponse = + transactionWrapper { + if (id == MkplAdId.NONE) return@transactionWrapper errorEmptyId + + val current = adTable.selectAll().where { adTable.id eq id.asString() } + .singleOrNull() + ?.let { adTable.from(it) } + + when { + current == null -> errorNotFound(id) + current.lock != lock -> errorRepoConcurrency(current, lock) + else -> block(current) + } + } + + + actual override suspend fun updateAd(rq: DbAdRequest): IDbAdResponse = update(rq.ad.id, rq.ad.lock) { + adTable.update({ adTable.id eq rq.ad.id.asString() }) { + to(it, rq.ad.copy(lock = MkplAdLock(randomUuid())), randomUuid) + } + read(rq.ad.id) + } + + actual override suspend fun deleteAd(rq: DbAdIdRequest): IDbAdResponse = update(rq.id, rq.lock) { + adTable.deleteWhere { id eq rq.id.asString() } + DbAdResponseOk(it) + } + + actual override suspend fun searchAd(rq: DbAdFilterRequest): IDbAdsResponse = + transactionWrapper({ + val res = adTable.selectAll().where { + buildList { + add(Op.TRUE) + if (rq.ownerId != MkplUserId.NONE) { + add(adTable.owner eq rq.ownerId.asString()) + } + if (rq.dealSide != MkplDealSide.NONE) { + add(adTable.dealSide eq rq.dealSide) + } + if (rq.titleFilter.isNotBlank()) { + add( + (adTable.title like "%${rq.titleFilter}%") + or (adTable.description like "%${rq.titleFilter}%") + ) + } + }.reduce { a, b -> a and b } + } + DbAdsResponseOk(data = res.map { adTable.from(it) }) + }, { + DbAdsResponseErr(it.asMkplError()) + }) +} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmMain/kotlin/VisiblityEnum.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmMain/kotlin/VisiblityEnum.kt new file mode 100644 index 0000000..e9f61e2 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmMain/kotlin/VisiblityEnum.kt @@ -0,0 +1,48 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import org.jetbrains.exposed.sql.Table +import org.postgresql.util.PGobject +import ru.otus.otuskotlin.marketplace.common.models.MkplVisibility + +fun Table.visibilityEnumeration( + columnName: String +) = customEnumeration( + name = columnName, + sql = SqlFields.VISIBILITY_TYPE, + fromDb = { value -> + when (value.toString()) { + SqlFields.VISIBILITY_OWNER -> MkplVisibility.VISIBLE_TO_OWNER + SqlFields.VISIBILITY_GROUP -> MkplVisibility.VISIBLE_TO_GROUP + SqlFields.VISIBILITY_PUBLIC -> MkplVisibility.VISIBLE_PUBLIC + else -> MkplVisibility.NONE + } + }, + toDb = { value -> + when (value) { +// MkplVisibility.VISIBLE_TO_OWNER -> PGobject().apply { type = SqlFields.VISIBILITY_TYPE; value = SqlFields.VISIBILITY_OWNER} + MkplVisibility.VISIBLE_TO_OWNER -> PgVisibilityOwner + MkplVisibility.VISIBLE_TO_GROUP -> PgVisibilityGroup + MkplVisibility.VISIBLE_PUBLIC -> PgVisibilityPublic + MkplVisibility.NONE -> throw Exception("Wrong value of Visibility. NONE is unsupported") + } + } +) + +sealed class PgVisibilityValue(eValue: String) : PGobject() { + init { + type = SqlFields.VISIBILITY_TYPE + value = eValue + } +} + +object PgVisibilityPublic: PgVisibilityValue(SqlFields.VISIBILITY_PUBLIC) { + private fun readResolve(): Any = PgVisibilityPublic +} + +object PgVisibilityOwner: PgVisibilityValue(SqlFields.VISIBILITY_OWNER) { + private fun readResolve(): Any = PgVisibilityOwner +} + +object PgVisibilityGroup: PgVisibilityValue(SqlFields.VISIBILITY_GROUP) { + private fun readResolve(): Any = PgVisibilityGroup +} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmTest/kotlin/GetEnv.jvm.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmTest/kotlin/GetEnv.jvm.kt new file mode 100644 index 0000000..48c3be2 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/jvmTest/kotlin/GetEnv.jvm.kt @@ -0,0 +1,3 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +actual fun getEnv(name: String): String? = System.getenv(name) diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Main/kotlin/RepoAdSql.linuxX64.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Main/kotlin/RepoAdSql.linuxX64.kt new file mode 100644 index 0000000..3b8a774 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Main/kotlin/RepoAdSql.linuxX64.kt @@ -0,0 +1,208 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import io.github.moreirasantos.pgkn.PostgresDriver +import io.github.moreirasantos.pgkn.resultset.ResultSet +import kotlinx.coroutines.runBlocking +import ru.otus.otuskotlin.marketplace.backend.repo.postgresql.SqlFields.quoted +import ru.otus.otuskotlin.marketplace.common.models.* +import ru.otus.otuskotlin.marketplace.common.repo.* +import ru.otus.otuskotlin.marketplace.repo.common.IRepoAdInitializable + +@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") +actual class RepoAdSql actual constructor( + properties: SqlProperties, + val randomUuid: () -> String, +) : IRepoAd, IRepoAdInitializable { + init { + require(properties.database.matches(Regex("^[\\w\\d_]+\$"))) { + "PostgreSQL database must contain only letters, numbers and underscore symbol '_'" + } + require(properties.schema.matches(Regex("^[\\w\\d_]+\$"))) { + "PostgreSQL schema must contain only letters, numbers and underscore symbol '_'" + } + require(properties.table.matches(Regex("^[\\w\\d_]+\$"))) { + "PostgreSQL table must contain only letters, numbers and underscore symbol '_'" + } + } + + private val dbName: String = "\"${properties.schema}\".\"${properties.table}\"".apply { + // Валидируем, что админ не ошибся в имени таблицы + } + + // private val driver by lazy { +// PostgresDriver( +// host = properties.host, +// port = properties.port, +// user = properties.user, +// database = properties.database, +// password = properties.password, +// ) +// } + init { + initConnection(properties) + } + + // insert into db (id) VALUES (:id) + // insert into db ("iD", iD) VALUES (:id) + private suspend fun saveElement(saveAd: MkplAd): IDbAdResponse { + val sql = """ + INSERT INTO $dbName ( + ${SqlFields.ID.quoted()}, + ${SqlFields.TITLE.quoted()}, + ${SqlFields.DESCRIPTION.quoted()}, + ${SqlFields.VISIBILITY.quoted()}, + ${SqlFields.AD_TYPE.quoted()}, + ${SqlFields.LOCK.quoted()}, + ${SqlFields.OWNER_ID.quoted()}, + ${SqlFields.PRODUCT_ID.quoted()} + ) VALUES ( + :${SqlFields.ID}, + :${SqlFields.TITLE}, + :${SqlFields.DESCRIPTION}, + :${SqlFields.VISIBILITY}::${SqlFields.VISIBILITY_TYPE}, + :${SqlFields.AD_TYPE}::${SqlFields.AD_TYPE_TYPE}, + :${SqlFields.LOCK}, + :${SqlFields.OWNER_ID}, + :${SqlFields.PRODUCT_ID} + ) + RETURNING ${SqlFields.allFields.joinToString()} + """.trimIndent() + val res = driver.execute( + sql = sql, + saveAd.toDb(), + ) { row: ResultSet -> row.fromDb(SqlFields.allFields) } + return DbAdResponseOk(res.first()) + } + + actual override fun save(ads: Collection): Collection = runBlocking { + ads.map { + val res = saveElement(it) + if (res !is DbAdResponseOk) throw Exception() + res.data + } + } + + actual override suspend fun createAd(rq: DbAdRequest): IDbAdResponse { + val saveAd = rq.ad.copy(id = MkplAdId(randomUuid()), lock = MkplAdLock(randomUuid())) + return saveElement(saveAd) + } + + actual override suspend fun readAd(rq: DbAdIdRequest): IDbAdResponse { + val sql = """ + SELECT ${SqlFields.allFields.joinToString { it.quoted() }} + FROM $dbName + WHERE ${SqlFields.ID.quoted()} = :${SqlFields.ID} + """.trimIndent() + val res = driver.execute( + sql = sql, + rq.id.toDb(), + ) { row: ResultSet -> row.fromDb(SqlFields.allFields) } + return if (res.isEmpty()) errorNotFound(rq.id) else DbAdResponseOk(res.first()) + } + + actual override suspend fun updateAd(rq: DbAdRequest): IDbAdResponse { + val sql = """ + WITH update_obj AS ( + UPDATE $dbName a + SET ${SqlFields.TITLE.quoted()} = :${SqlFields.TITLE} + , ${SqlFields.DESCRIPTION.quoted()} = :${SqlFields.DESCRIPTION} + , ${SqlFields.AD_TYPE.quoted()} = :${SqlFields.AD_TYPE}::${SqlFields.AD_TYPE_TYPE} + , ${SqlFields.VISIBILITY.quoted()} = :${SqlFields.VISIBILITY}::${SqlFields.VISIBILITY_TYPE} + , ${SqlFields.LOCK.quoted()} = :${SqlFields.LOCK} + , ${SqlFields.OWNER_ID.quoted()} = :${SqlFields.OWNER_ID} + , ${SqlFields.PRODUCT_ID.quoted()} = :${SqlFields.PRODUCT_ID} + WHERE a.${SqlFields.ID.quoted()} = :${SqlFields.ID} + AND a.${SqlFields.LOCK.quoted()} = :${SqlFields.LOCK_OLD} + RETURNING ${SqlFields.allFields.joinToString()} + ), + select_obj AS ( + SELECT ${SqlFields.allFields.joinToString()} FROM $dbName + WHERE ${SqlFields.ID.quoted()} = :${SqlFields.ID} + ) + (SELECT * FROM update_obj UNION ALL SELECT * FROM select_obj) LIMIT 1 + """.trimIndent() + val rqAd = rq.ad + val newAd = rqAd.copy(lock = MkplAdLock(randomUuid())) + val res = driver.execute( + sql = sql, + newAd.toDb() + rqAd.lock.toDb(), + ) { row: ResultSet -> row.fromDb(SqlFields.allFields) } + val returnedAd: MkplAd? = res.firstOrNull() + return when { + returnedAd == null -> errorNotFound(rq.ad.id) + returnedAd.lock == newAd.lock -> DbAdResponseOk(returnedAd) + else -> errorRepoConcurrency(returnedAd, rqAd.lock) + } + } + + actual override suspend fun deleteAd(rq: DbAdIdRequest): IDbAdResponse { + val sql = """ + WITH delete_obj AS ( + DELETE FROM $dbName a + WHERE a.${SqlFields.ID.quoted()} = :${SqlFields.ID} + AND a.${SqlFields.LOCK.quoted()} = :${SqlFields.LOCK_OLD} + RETURNING '${SqlFields.DELETE_OK}' + ) + SELECT ${SqlFields.allFields.joinToString()}, (SELECT * FROM delete_obj) as flag FROM $dbName + WHERE ${SqlFields.ID.quoted()} = :${SqlFields.ID} + """.trimIndent() + val res = driver.execute( + sql = sql, + rq.id.toDb() + rq.lock.toDb(), + ) { row: ResultSet -> row.fromDb(SqlFields.allFields) to row.getString(SqlFields.allFields.size) } + val returnedPair: Pair? = res.firstOrNull() + val returnedAd: MkplAd? = returnedPair?.first + return when { + returnedAd == null -> errorNotFound(rq.id) + returnedPair.second == SqlFields.DELETE_OK -> DbAdResponseOk(returnedAd) + else -> errorRepoConcurrency(returnedAd, rq.lock) + } + } + + actual override suspend fun searchAd(rq: DbAdFilterRequest): IDbAdsResponse { + val where = listOfNotNull( + rq.ownerId.takeIf { it != MkplUserId.NONE } + ?.let { "${SqlFields.OWNER_ID.quoted()} = :${SqlFields.OWNER_ID}" }, + rq.dealSide.takeIf { it != MkplDealSide.NONE } + ?.let { "${SqlFields.AD_TYPE.quoted()} = :${SqlFields.AD_TYPE}::${SqlFields.AD_TYPE_TYPE}" }, + rq.titleFilter.takeIf { it.isNotBlank() } + ?.let { "${SqlFields.TITLE.quoted()} LIKE :${SqlFields.TITLE}" }, + ) + .takeIf { it.isNotEmpty() } + ?.let { "WHERE ${it.joinToString(separator = " AND ")}" } + ?: "" + + val sql = """ + SELECT ${SqlFields.allFields.joinToString { it.quoted() }} + FROM $dbName $where + """.trimIndent() + println("SQL: $sql") + val res = driver.execute( + sql = sql, + rq.toDb(), + ) { row: ResultSet -> row.fromDb(SqlFields.allFields) } + return DbAdsResponseOk(res) + } + + actual fun clear(): Unit = runBlocking { + val sql = """ + DELETE FROM $dbName + """.trimIndent() + driver.execute(sql = sql) + } + + companion object { + private lateinit var driver: PostgresDriver + private fun initConnection(properties: SqlProperties) { + if (!this::driver.isInitialized) { + driver = PostgresDriver( + host = properties.host, + port = properties.port, + user = properties.user, + database = properties.database, + password = properties.password, + ) + } + } + } +} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Main/kotlin/SqlMapperFromDb.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Main/kotlin/SqlMapperFromDb.kt new file mode 100644 index 0000000..56db731 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Main/kotlin/SqlMapperFromDb.kt @@ -0,0 +1,32 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import io.github.moreirasantos.pgkn.resultset.ResultSet +import ru.otus.otuskotlin.marketplace.common.models.* + +internal fun ResultSet.fromDb(cols: List): MkplAd { + val fieldsMap = cols.mapIndexed { i: Int, field: String -> field to i }.toMap() + fun col(field: String): String? = fieldsMap[field]?.let { getString(it) } + return MkplAd( + id = col(SqlFields.ID)?.let { MkplAdId(it) } ?: MkplAdId.NONE, + title = col(SqlFields.TITLE) ?: "", + description = col(SqlFields.DESCRIPTION) ?: "", + ownerId = col(SqlFields.OWNER_ID)?.let { MkplUserId(it) } ?: MkplUserId.NONE, + adType = col(SqlFields.AD_TYPE).asAdType(), + visibility = col(SqlFields.VISIBILITY).asVisibility(), + productId = col(SqlFields.PRODUCT_ID)?.let { MkplProductId(it) } ?: MkplProductId.NONE, + lock = col(SqlFields.LOCK)?.let { MkplAdLock(it) } ?: MkplAdLock.NONE, + ) +} + +private fun String?.asAdType(): MkplDealSide = when (this) { + SqlFields.AD_TYPE_DEMAND -> MkplDealSide.DEMAND + SqlFields.AD_TYPE_SUPPLY -> MkplDealSide.SUPPLY + else -> MkplDealSide.NONE +} + +private fun String?.asVisibility(): MkplVisibility = when (this) { + SqlFields.VISIBILITY_OWNER -> MkplVisibility.VISIBLE_TO_OWNER + SqlFields.VISIBILITY_GROUP -> MkplVisibility.VISIBLE_TO_GROUP + SqlFields.VISIBILITY_PUBLIC -> MkplVisibility.VISIBLE_PUBLIC + else -> MkplVisibility.NONE +} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Main/kotlin/SqlMapperToDb.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Main/kotlin/SqlMapperToDb.kt new file mode 100644 index 0000000..d952750 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Main/kotlin/SqlMapperToDb.kt @@ -0,0 +1,43 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import ru.otus.otuskotlin.marketplace.common.models.* +import ru.otus.otuskotlin.marketplace.common.repo.DbAdFilterRequest + +private fun String.toDb() = this.takeIf { it.isNotBlank() } + +internal fun MkplAdId.toDb() = mapOf( + SqlFields.ID to asString().toDb(), +) +internal fun MkplAdLock.toDb() = mapOf( + SqlFields.LOCK_OLD to asString().toDb(), +) + +internal fun MkplAd.toDb() = id.toDb() + mapOf( + SqlFields.TITLE to title.toDb(), + SqlFields.DESCRIPTION to description.toDb(), + SqlFields.OWNER_ID to ownerId.asString().toDb(), + SqlFields.AD_TYPE to adType.toDb(), + SqlFields.VISIBILITY to visibility.toDb(), + SqlFields.PRODUCT_ID to productId.asString().toDb(), + SqlFields.LOCK to lock.asString().toDb(), +) + +internal fun DbAdFilterRequest.toDb() = mapOf( + // Используется для LIKE '%titleFilter% + SqlFields.FILTER_TITLE to titleFilter.toDb()?.let { "%$it%"}, + SqlFields.FILTER_AD_TYPE to dealSide.toDb(), + SqlFields.FILTER_OWNER_ID to ownerId.asString().toDb(), +) + +private fun MkplDealSide.toDb() = when (this) { + MkplDealSide.DEMAND -> SqlFields.AD_TYPE_DEMAND + MkplDealSide.SUPPLY -> SqlFields.AD_TYPE_SUPPLY + MkplDealSide.NONE -> null +} + +private fun MkplVisibility.toDb() = when (this) { + MkplVisibility.VISIBLE_TO_OWNER -> SqlFields.VISIBILITY_OWNER + MkplVisibility.VISIBLE_TO_GROUP -> SqlFields.VISIBILITY_GROUP + MkplVisibility.VISIBLE_PUBLIC -> SqlFields.VISIBILITY_PUBLIC + MkplVisibility.NONE -> null +} diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Test/kotlin/GetEnv.linuxX64.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Test/kotlin/GetEnv.linuxX64.kt new file mode 100644 index 0000000..2370afb --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Test/kotlin/GetEnv.linuxX64.kt @@ -0,0 +1,8 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import platform.posix.getenv + +@OptIn(ExperimentalForeignApi::class) +actual fun getEnv(name: String): String? = getenv("postgresPort")?.toKString() diff --git a/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Test/kotlin/RepoAdSqlTest.linuxX64.kt b/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Test/kotlin/RepoAdSqlTest.linuxX64.kt new file mode 100644 index 0000000..f9162a5 --- /dev/null +++ b/ok-marketplace-be/ok-marketplace-repo-postgres/src/linuxX64Test/kotlin/RepoAdSqlTest.linuxX64.kt @@ -0,0 +1,54 @@ +package ru.otus.otuskotlin.marketplace.backend.repo.postgresql + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import kotlinx.coroutines.test.runTest +import platform.posix.getenv +import ru.otus.otuskotlin.marketplace.common.models.* +import ru.otus.otuskotlin.marketplace.common.repo.DbAdFilterRequest +import ru.otus.otuskotlin.marketplace.common.repo.DbAdRequest +import kotlin.test.Test + +class RepoAdSqlTest { + @OptIn(ExperimentalForeignApi::class) + @Test + fun create() = runTest { + val pgPort = getenv("postgresPort")?.toKString()?.toIntOrNull() ?: 5432 + + val repo = RepoAdSql( + properties = SqlProperties(port = pgPort) + ) + val res = repo.createAd( + rq = DbAdRequest( + MkplAd( + title = "tttt", + description = "zzzz", + visibility = MkplVisibility.VISIBLE_PUBLIC, + adType = MkplDealSide.DEMAND, + ownerId = MkplUserId("1234"), + productId = MkplProductId("4567"), + lock = MkplAdLock("235356"), + ) + ) + ) + println("CREATED $res") + } + + @OptIn(ExperimentalForeignApi::class) + @Test + fun search() = runTest { + val pgPort = getenv("postgresPort")?.toKString()?.toIntOrNull() ?: 5432 + + val repo = RepoAdSql( + properties = SqlProperties(port = pgPort) + ) + val res = repo.searchAd( + rq = DbAdFilterRequest( + titleFilter = "tttt", + dealSide = MkplDealSide.DEMAND, + ownerId = MkplUserId("1234"), + ) + ) + println("SEARCH $res") + } +} diff --git a/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdReadTest.kt b/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdReadTest.kt index 5fe67de..d1b168c 100644 --- a/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdReadTest.kt +++ b/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdReadTest.kt @@ -2,6 +2,7 @@ package ru.otus.otuskotlin.marketplace.backend.repo.tests import ru.otus.otuskotlin.marketplace.common.models.MkplAd import ru.otus.otuskotlin.marketplace.common.models.MkplAdId +import ru.otus.otuskotlin.marketplace.common.models.MkplError import ru.otus.otuskotlin.marketplace.common.repo.DbAdIdRequest import ru.otus.otuskotlin.marketplace.common.repo.DbAdResponseErr import ru.otus.otuskotlin.marketplace.common.repo.DbAdResponseOk @@ -25,14 +26,17 @@ abstract class RepoAdReadTest { @Test fun readNotFound() = runRepoTest { + println("REQUESTING") val result = repo.readAd(DbAdIdRequest(notFoundId)) + println("RESULT: $result") assertIs(result) - val error = result.errors.find { it.code == "repo-not-found" } + println("ERRORS: ${result.errors}") + val error: MkplError? = result.errors.find { it.code == "repo-not-found" } assertEquals("id", error?.field) } - companion object : BaseInitAds("delete") { + companion object : BaseInitAds("read") { override val initObjects: List = listOf( createInitTestModel("read") ) diff --git a/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdUpdateTest.kt b/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdUpdateTest.kt index c501f19..ceac5a5 100644 --- a/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdUpdateTest.kt +++ b/ok-marketplace-be/ok-marketplace-repo-tests/src/commonMain/kotlin/RepoAdUpdateTest.kt @@ -50,6 +50,8 @@ abstract class RepoAdUpdateTest { @Test fun updateSuccess() = runRepoTest { val result = repo.updateAd(DbAdRequest(reqUpdateSucc)) + println("ERRORS: ${(result as? DbAdResponseErr)?.errors}") + println("ERRORSWD: ${(result as? DbAdResponseErrWithData)?.errors}") assertIs(result) assertEquals(reqUpdateSucc.id, result.data.id) assertEquals(reqUpdateSucc.title, result.data.title) diff --git a/ok-marketplace-be/ok-marketplace-stubs/src/commonMain/kotlin/MkplAdStubBolts.kt b/ok-marketplace-be/ok-marketplace-stubs/src/commonMain/kotlin/MkplAdStubBolts.kt index f08a216..6549ca0 100644 --- a/ok-marketplace-be/ok-marketplace-stubs/src/commonMain/kotlin/MkplAdStubBolts.kt +++ b/ok-marketplace-be/ok-marketplace-stubs/src/commonMain/kotlin/MkplAdStubBolts.kt @@ -7,7 +7,7 @@ object MkplAdStubBolts { get() = MkplAd( id = MkplAdId("666"), title = "Требуется болт", - description = "Требуется болт 100x5 с шистигранной шляпкой", + description = "Требуется болт 100x5 с шестигранной шляпкой", ownerId = MkplUserId("user-1"), adType = MkplDealSide.DEMAND, visibility = MkplVisibility.VISIBLE_PUBLIC, diff --git a/ok-marketplace-be/settings.gradle.kts b/ok-marketplace-be/settings.gradle.kts index b88fbe4..d51c82a 100644 --- a/ok-marketplace-be/settings.gradle.kts +++ b/ok-marketplace-be/settings.gradle.kts @@ -49,4 +49,5 @@ include(":ok-marketplace-repo-common") include(":ok-marketplace-repo-inmemory") include(":ok-marketplace-repo-stubs") include(":ok-marketplace-repo-tests") +include(":ok-marketplace-repo-postgres") diff --git a/ok-marketplace-tests/build.gradle.kts b/ok-marketplace-tests/build.gradle.kts index e5a9767..9112c5e 100644 --- a/ok-marketplace-tests/build.gradle.kts +++ b/ok-marketplace-tests/build.gradle.kts @@ -30,4 +30,7 @@ tasks { dependsOn(subprojects.map { it.getTasksByName(tsk,false)}) } } + create("e2eTests") { + dependsOn(project(":ok-marketplace-e2e-be").tasks.getByName("check")) + } } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/build.gradle.kts b/ok-marketplace-tests/ok-marketplace-e2e-be/build.gradle.kts index 8f34904..dd397ea 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/build.gradle.kts +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/build.gradle.kts @@ -32,7 +32,7 @@ tasks { withType().configureEach { useJUnitPlatform() // dependsOn(gradle.includedBuild(":ok-marketplace-app-spring").task("dockerBuildImage")) -// dependsOn(gradle.includedBuild(":ok-marketplace-app-ktor").task("publishImageToLocalRegistry")) +// dependsOn(gradle.includedBuild(":ok-marketplace-be").task(":ok-marketplace-app-ktor:publishImageToLocalRegistry")) // dependsOn(gradle.includedBuild(":ok-marketplace-app-rabbit").task("dockerBuildImage")) // dependsOn(gradle.includedBuild(":ok-marketplace-app-kafka").task("dockerBuildImage")) } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-ktor-pg-jvm.yml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-ktor-pg-jvm.yml new file mode 100644 index 0000000..aab1c54 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-ktor-pg-jvm.yml @@ -0,0 +1,48 @@ +# Конфигурация для spring + (в перспективе) postgresql + +version: '3.9' +services: + app-ktor: + image: ok-marketplace-app-ktor:0.0.1 + ports: + - "8080:8080" + environment: + DB_TYPE_PROD: "psql" + MKPLADS_HOST: "psql" + MKPLADS_PORT: 5432 + MKPLADS_DB: "marketplace_ads" + MKPLADS_USER: "postgres" + MKPLADS_PASS: "marketplace-pass" + depends_on: + psql: + condition: service_healthy + liquibase: + condition: service_completed_successfully + + psql: + image: postgres +# volumes: +# - postgres_data:/var/lib/postgresql/data +# ports: +# - "5432:5432" + environment: + POSTGRES_PASSWORD: "marketplace-pass" + POSTGRES_USER: "postgres" + POSTGRES_DB: "marketplace_ads" + healthcheck: + test: [ "CMD-SHELL", "pg_isready" ] + interval: 10s + timeout: 5s + retries: 5 + + liquibase: + image: liquibase/liquibase + volumes: + - ./volumes/liquibase-psql:/liquibase/changelog + depends_on: + psql: + condition: service_healthy + command: ["--defaults-file=/liquibase/changelog/liquibase.properties", "--search-path=/liquibase/changelog", "--url=jdbc:postgresql://psql:5432/marketplace_ads", "update"] + +#volumes: +# postgres_data: diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-ktor-pg-linux.yml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-ktor-pg-linux.yml new file mode 100644 index 0000000..6a5c265 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-ktor-pg-linux.yml @@ -0,0 +1,47 @@ +version: '3.9' + +services: + app-ktor: + image: ok-marketplace-app-ktor-x64:0.0.1 + ports: + - "8080:8080" + environment: + DB_TYPE_PROD: "psql" + MKPLADS_HOST: "psql" + MKPLADS_PORT: 5432 + MKPLADS_DB: "marketplace_ads" + MKPLADS_USER: "postgres" + MKPLADS_PASS: "marketplace-pass" + depends_on: + psql: + condition: service_healthy + liquibase: + condition: service_completed_successfully + + psql: + image: postgres + # volumes: +# - postgres_data:/var/lib/postgresql/data +# ports: +# - "5432:5432" + environment: + POSTGRES_PASSWORD: "marketplace-pass" + POSTGRES_USER: "postgres" + POSTGRES_DB: "marketplace_ads" + healthcheck: + test: [ "CMD-SHELL", "pg_isready" ] + interval: 10s + timeout: 5s + retries: 5 + + liquibase: + image: liquibase/liquibase + volumes: + - ./volumes/liquibase-psql:/liquibase/changelog + depends_on: + psql: + condition: service_healthy + command: ["--defaults-file=/liquibase/changelog/liquibase.properties", "--search-path=/liquibase/changelog", "--url=jdbc:postgresql://psql:5432/marketplace_ads", "update"] + +#volumes: +# postgres_data: diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-pg.yml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-pg.yml new file mode 100644 index 0000000..a083f9e --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-pg.yml @@ -0,0 +1,29 @@ +version: '3.9' +services: + psql: + image: postgres + # volumes: +# - postgres_data:/var/lib/postgresql/data +# ports: +# - "5432:5432" + environment: + POSTGRES_PASSWORD: "marketplace-pass" + POSTGRES_USER: "postgres" + POSTGRES_DB: "marketplace_ads" + healthcheck: + test: [ "CMD-SHELL", "pg_isready" ] + interval: 10s + timeout: 5s + retries: 5 + + liquibase: + image: liquibase/liquibase + volumes: + - ./volumes/liquibase-psql:/liquibase/changelog + depends_on: + psql: + condition: service_healthy + command: ["--defaults-file=/liquibase/changelog/liquibase.properties", "--search-path=/liquibase/changelog", "--url=jdbc:postgresql://psql:5432/marketplace_ads", "update"] + +#volumes: +# postgres_data: diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-spring-pg.yml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-spring-pg.yml new file mode 100644 index 0000000..fcd5951 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-spring-pg.yml @@ -0,0 +1,47 @@ +version: '3.9' + +services: + app-spring: + image: ok-marketplace-app-spring:0.0.1 + ports: + - "8080:8080" + environment: + DB_TYPE_PROD: "psql" + MKPLADS_HOST: "psql" + MKPLADS_PORT: 5432 + MKPLADS_DB: "marketplace_ads" + MKPLADS_USER: "postgres" + MKPLADS_PASS: "marketplace-pass" + depends_on: + psql: + condition: service_healthy + liquibase: + condition: service_completed_successfully + + psql: + image: postgres + # volumes: + # - postgres_data:/var/lib/postgresql/data + # ports: + # - "5432:5432" + environment: + POSTGRES_PASSWORD: "marketplace-pass" + POSTGRES_USER: "postgres" + POSTGRES_DB: "marketplace_ads" + healthcheck: + test: [ "CMD-SHELL", "pg_isready" ] + interval: 10s + timeout: 5s + retries: 5 + + liquibase: + image: liquibase/liquibase + volumes: + - ./volumes/liquibase-psql:/liquibase/changelog + depends_on: + psql: + condition: service_healthy + command: ["--defaults-file=/liquibase/changelog/liquibase.properties", "--search-path=/liquibase/changelog", "--url=jdbc:postgresql://psql:5432/marketplace_ads", "update"] + +#volumes: +# postgres_data: diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-spring.yml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-spring.yml deleted file mode 100644 index 0b7fa2c..0000000 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/docker-compose-spring.yml +++ /dev/null @@ -1,8 +0,0 @@ -# Конфигурация для spring + (в перспективе) postgresql - -version: '3' -services: - app-spring: - image: ok-marketplace-app-spring:latest - ports: - - "8080:8080" diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/changelog-v0.0.1.sql b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/changelog-v0.0.1.sql new file mode 100644 index 0000000..dc1db71 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/changelog-v0.0.1.sql @@ -0,0 +1,24 @@ +--liquibase formatted sql + +--changeset sokatov:1 labels:v0.0.1 +CREATE TYPE "ad_types_type" AS ENUM ('demand', 'supply'); +CREATE TYPE "ad_visibilities_type" AS ENUM ('public', 'owner', 'group'); + +CREATE TABLE "ads" ( + "id" text primary key constraint ads_id_length_ctr check (length("id") < 64), + "title" text constraint ads_title_length_ctr check (length(title) < 128), + "description" text constraint ads_description_length_ctr check (length(title) < 4096), + "ad_type" ad_types_type not null, + "visibility" ad_visibilities_type not null, + "owner_id" text not null constraint ads_owner_id_length_ctr check (length(id) < 64), + "product_id" text constraint ads_product_id_length_ctr check (length(id) < 64), + "lock" text not null constraint ads_lock_length_ctr check (length(id) < 64) +); + +CREATE INDEX ads_owner_id_idx on "ads" using hash ("owner_id"); + +CREATE INDEX ads_product_id_idx on "ads" using hash ("product_id"); + +CREATE INDEX ads_ad_type_idx on "ads" using hash ("ad_type"); + +CREATE INDEX ads_visibility_idx on "ads" using hash ("visibility"); diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.advanced.flowfile.yaml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.advanced.flowfile.yaml new file mode 100644 index 0000000..840defe --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.advanced.flowfile.yaml @@ -0,0 +1,147 @@ +########## LIQUIBASE FLOW FILE ########## +########## learn more http://docs.liquibase.com/flow ########## + +## NOTE: This is an advanced example flowfile, compared to the other sample at examples/liquibase.flowfile.yaml +#### HOW TO USE THIS FILE: +#### example for CLI: liquibase flow --flow-file=liquibase.advanced.flowfile.yaml +#### example for ENV Var: LIQUIBASE_FLOW_FLOW_FILE=liquibase.advanced.flowfile.yaml + +## Advanced options show in this file include: +#### non-default name of 'liquibase.advanced.flowfile.yaml' (use by setting flowfile property to this name) +#### use of 'include' to inject namespaced yaml files of key: val variables +#### use of globalVariables and stageVariables +#### use of globalArgs and cmdArgs +#### use of property substitution +#### use of a nested flowfile (in this case in the endStage, but could be elsewhere) +#### use of if: conditional which allows a -type: shell or -type: liquibase command to run +###### In the example below, we set an environment variable LIQUIBASE_CURRENT_TARGET, such as 'export LIQUIBASE_CURRENT_TARGET=dev' +###### This could be determined dynamically, of course, from the build tools, bu tthis is simpler for this example "if:" conditional +#### use of shell commands in a -type: shell block. +###### command: bash -c "the shell command || and its chained commands && go in the quotes" +######## +#### POTENTIAL use of environment variables: +###### DATETIME STAMP +######## In this file, you could replace ${FLOWVARS.THISDATE} with an env var, such as ${LIQUIBASE_THISDATE} set via .bash_profile +######## for example 'export LIQUIBASE_THISDATE=$( date +'%Y-%m-%dT%H-%M-%S' )' + + +## Bring in and namespace an external file with yaml 'key: val' pairs for use in this file +## The variables will be used as ${namespace.variablename}, seen in this example as ${FLOWVARS.PROJNAME} +include: + FLOWVARS: liquibase.flowvariables.yaml + + + +## Set up some global variables for property substitution in ANY stage +globalVariables: + DIRNAME: "./${FLOWVARS.PROJNAME}_${FLOWVARS.THISDATE}" + STATUSFILE: "status.txt" + UPDATELOG: "update.log" + HISTORYFILE: "history.txt" + + +## Start of the stages. +stages: + + ## A prep stage. There can be more than one stage if desired. + stage-prep: + + actions: + + - type: shell + command: bash -c "mkdir -p ${DIRNAME}" + + + ## Another stage. + stage-dowork: + + ## set up vars for property substitution in THIS stage only + stageVariables: + + VERBOSESTATE: TRUE + + + actions: + # + # Do a validate command + # + - type: liquibase + command: validate + + # + # Tell me what is pending a deployment + # + - type: shell + command: bash -c "liquibase --show-banner false --outputfile ./${DIRNAME}/${STATUSFILE} status --verbose ${VERBOSESTATE}" + + # This is the structured way to setup a liquibase command, if you dont want to run it as one 'bash -c' command + #- type: liquibase + # command: status + # globalArgs: + # outputfile: "${DIRNAME}/${STATUSFILE}" + # showbanner: false + # cmdArgs: {verbose: "${VERBOSESTATE}" + + + + + # + # And then save a version in detail, if env var LIQUIBASE_FILE_OUTPUT == 1 + # + - type: shell + command: bash -c "echo 'LIQUIBASE_ env vars ' && env | grep 'LIQUIBASE_' " + + - type: liquibase + ## if this var LIQUIBASE_CURRENT_TARGET is "dev", then the updatesql will run + if: "${LIQUIBASE_CURRENT_TARGET} == dev" + command: updatesql + globalArgs: {outputfile: "${DIRNAME}/${UPDATELOG}"} + + - type: shell + ## if this var LIQUIBASE_CURRENT_TARGET is not "dev", then the message will be displayed + if: "${LIQUIBASE_CURRENT_TARGET} != dev" + command: echo "No output files created. Set env var LIQUIBASE_CURRENT_TARGET to dev to trigger file creation." + + + + # + # Quality Checks for changelog + # + - type: liquibase + command: checks run + cmdArgs: {checks-scope: changelog} + + # + # Run update + # + - type: liquibase + command: update + + + # + # Quality Checks for database + # + - type: liquibase + command: checks run + cmdArgs: {checks-scope: database} + + + # + # Create a history file + # + - type: liquibase + command: history + globalArgs: {outputfile: "${DIRNAME}/${HISTORYFILE}"} + + + +## The endStage ALWAYS RUNS. +## So put actions here which you desire to perform whether previous stages' actions succeed or fail. +## If you do not want any actions to ALWAYS RUN, simply delete the endStage from your flow file. + +endStage: + actions: + - type: liquibase + ## Notice this is a flow command in a flow file, and it called a 'nested' flowfile, which in this case lives in the same dir, but could be elsewhere + command: flow + cmdArgs: {flowfile: liquibase.endstage.flow} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.checks-package.yaml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.checks-package.yaml new file mode 100644 index 0000000..887d0b8 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.checks-package.yaml @@ -0,0 +1,14 @@ +## This is a Liquibase checks packages formatted yaml file. +## It contains just one or more 'checksPackages' objects +## with one or more named checks-settings-files. +## These checks-settings-files listed in a package are processed sequentially +## during 'liquibase checks run' or 'liquibase checks show' commands +## which specify this file as the checks-settings-file property. +## Learn more at https://docs.liquibase.com/quality-checks + +## Uncomment and change names with your checks-settings-files +#checksPackages: +# - name: "my-checks-files.pkg" +# files: +# - "./liquibase.checks-settings.conf" +# - "./liquibase.more.checks-settings.yaml" \ No newline at end of file diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.endstage.flow b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.endstage.flow new file mode 100644 index 0000000..f44af55 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.endstage.flow @@ -0,0 +1,31 @@ +########## LIQUIBASE FLOW FILE ########## +########## learn more http://docs.liquibase.com/flow ########## + +## NOTE: This example flowfile is called from the examples/liquibase.advanced.flowfile.yaml file +## While it could be run on its own, this file is designed to show that flow-files can be decomposed +## into separate files as makes sense for your use cases. + +stages: + + cleanuptheDB: + + actions: + + # + # Clear out the database + # + - type: liquibase + command: dropAll + + # + # Check that database is empty by seeing what is ready to be deployed + # + - type: liquibase + command: status + cmdArgs: {verbose: TRUE} + + +## The endStage ALWAYS RUNS. +## So put actions here which you desire to perform whether previous stages' actions succeed or fail. +## If you do not want any actions to ALWAYS RUN, simply delete the endStage from your flow file, +## as it has been deleted here in this liquibase.endStage.flow file. diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.flowfile.yaml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.flowfile.yaml new file mode 100644 index 0000000..58419bb --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.flowfile.yaml @@ -0,0 +1,41 @@ +########## LIQUIBASE FLOWFILE ########## +########## learn more http://docs.liquibase.com/flow ########## + +## Note: Any command which fails in any stage below result in the command stopping, and endStage being run. +## A flow file can have one or more stages, each with multiple "actions", +## or your flow file can have multiple stages with fewer actions in each stage. +stages: + + + ## The first stage of actions. + Default: + + actions: + # + # Quality Checks for changelog + # + - type: liquibase + command: checks run + cmdArgs: {checks-scope: changelog} + # + # Run the update + # + - type: liquibase + command: update + + # + # Quality Checks for database + # + - type: liquibase + command: checks run + cmdArgs: {checks-scope: database} + + +## The endStage ALWAYS RUNS. +## So put actions here which you desire to perform whether previous stages' actions succeed or fail. +## If you do not want any actions to ALWAYS RUN, simply delete the endStage from your flow file. + +endStage: + actions: + - type: liquibase + command: history diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.flowvariables.yaml b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.flowvariables.yaml new file mode 100644 index 0000000..7b41bb1 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.flowvariables.yaml @@ -0,0 +1,8 @@ +########## LIQUIBASE FLOWFILE ########## +########## learn more http://docs.liquibase.com/flow ########## + +## NOTE: This example yaml file of key:value variables is injected into examples/liquibase.advanced.flow file +## using the "include" ability. It will be given the namespace "DATES" but could be given any namespace. + +PROJNAME: "MyFlowProj" +THISDATE: "2022-11-28T15-00-20" \ No newline at end of file diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.properties b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.properties new file mode 100644 index 0000000..5089d45 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/liquibase-psql/liquibase.properties @@ -0,0 +1,64 @@ +#### _ _ _ _ +## | | (_) (_) | +## | | _ __ _ _ _ _| |__ __ _ ___ ___ +## | | | |/ _` | | | | | '_ \ / _` / __|/ _ \ +## | |___| | (_| | |_| | | |_) | (_| \__ \ __/ +## \_____/_|\__, |\__,_|_|_.__/ \__,_|___/\___| +## | | +## |_| +## +## The liquibase.properties file stores properties which do not change often, +## such as database connection information. Properties stored here save time +## and reduce risk of mistyped command line arguments. +## Learn more: https://docs.liquibase.com/concepts/connections/creating-config-properties.html +#### +#### +## Note about relative and absolute paths: +## The liquibase.properties file requires paths for some properties. +## The classpath is the path/to/resources (ex. src/main/resources). +## The changeLogFile path is relative to the classpath. +## The url H2 example below is relative to 'pwd' resource. +#### +# Enter the path for your changelog file. +changeLogFile=changelog-v0.0.1.sql + +#### Enter the Target database 'url' information #### +#liquibase.command.url=jdbc:postgresql://localhost:5432/marketplace_ads +#liquibase.command.url=jdbc:postgresql://localhost:32794/marketplace_ads + +# Enter the username for your Target database. +liquibase.command.username: postgres + +# Enter the password for your Target database. +liquibase.command.password: marketplace-pass + +#### Enter the Source Database 'referenceUrl' information #### +## The source database is the baseline or reference against which your target database is compared for diff/diffchangelog commands. + +# Enter URL for the source database +liquibase.command.referenceUrl: jdbc:postgresql://psql:5432/marketplace_ads + +# Enter the username for your source database +liquibase.command.referenceUsername: postgres + +# Enter the password for your source database +liquibase.command.referencePassword: marketplace-pass + +# Logging Configuration +# logLevel controls the amount of logging information generated. If not set, the default logLevel is INFO. +# Valid values, from least amount of logging to most, are: +# OFF, ERROR, WARN, INFO, DEBUG, TRACE, ALL +# If you are having problems, setting the logLevel to DEBUG and re-running the command can be helpful. +# logLevel: DEBUG + +# The logFile property controls where logging messages are sent. If this is not set, then logging messages are +# displayed on the console. If this is set, then messages will be sent to a file with the given name. +# logFile: liquibase.log + +#### Liquibase Pro Key Information #### +# Learn more, contact support, or get or renew a Pro Key at https://www.liquibase.com/trial +# liquibase.licenseKey: + +## Get documentation at docs.liquibase.com ## +## Get certified courses at learn.liquibase.com ## +## Get support at liquibase.com/support ## diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-create.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-create.json index a35cc86..d33e9aa 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-create.json +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-create.json @@ -7,7 +7,6 @@ "response": { "status": 200, "jsonBody": { - "responseType": "create", "result": "success", "ad": { "id": "123", diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-delete.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-delete.json index a3d7839..e85d521 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-delete.json +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-delete.json @@ -6,7 +6,6 @@ "response": { "status": 200, "jsonBody": { - "responseType": "delete", "result": "success", "ad": { "id": "{{{jsonPath request.body '$.ad.id'}}}", diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-offers.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-offers.json index c38adf7..a29e2af 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-offers.json +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-offers.json @@ -6,7 +6,6 @@ "response": { "status": 200, "jsonBody": { - "responseType": "offers", "result": "success", "ad": { "id": "123", diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-read.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-read.json index cfeff7a..63d20f0 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-read.json +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-read.json @@ -7,7 +7,6 @@ "response": { "status": 200, "jsonBody": { - "responseType": "read", "result": "success", "ad": { "id": "{{{jsonPath request.body '$.ad.id'}}}", diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-bolt.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-bolt.json index 717c318..595691a 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-bolt.json +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-bolt.json @@ -9,7 +9,6 @@ "response": { "status": 200, "jsonBody": { - "responseType": "search", "result": "success", "ads": [ { diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-selling.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-selling.json index 5d883fc..3811c3f 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-selling.json +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-search-selling.json @@ -9,7 +9,6 @@ "response": { "status": 200, "jsonBody": { - "responseType": "search", "result": "success", "ads": [ { diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-update.json b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-update.json index 48f5d28..94467ba 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-update.json +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/docker-compose/volumes/wm-marketplace/mappings/v2-update.json @@ -7,7 +7,6 @@ "response": { "status": 200, "jsonBody": { - "responseType": "update", "result": "success", "ad": { "id": "123", diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/KtorJvmPGDockerCompose.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/KtorJvmPGDockerCompose.kt new file mode 100644 index 0000000..5deed06 --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/KtorJvmPGDockerCompose.kt @@ -0,0 +1,7 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.docker + +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.docker.AbstractDockerCompose + +object KtorJvmPGDockerCompose : AbstractDockerCompose( + "app-ktor_1", 8080, "docker-compose-ktor-pg-jvm.yml" +) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/KtorLinuxPGDockerCompose.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/KtorLinuxPGDockerCompose.kt new file mode 100644 index 0000000..b5e45ff --- /dev/null +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/KtorLinuxPGDockerCompose.kt @@ -0,0 +1,7 @@ +package ru.otus.otuskotlin.marketplace.e2e.be.docker + +import ru.otus.otuskotlin.marketplace.e2e.be.fixture.docker.AbstractDockerCompose + +object KtorLinuxPGDockerCompose : AbstractDockerCompose( + "app-ktor_1", 8080, "docker-compose-ktor-pg-linux.yml" +) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/SpringDockerCompose.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/SpringDockerCompose.kt index 1490594..0e658c8 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/SpringDockerCompose.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/docker/SpringDockerCompose.kt @@ -3,5 +3,5 @@ package ru.otus.otuskotlin.marketplace.e2e.be.docker import ru.otus.otuskotlin.marketplace.e2e.be.fixture.docker.AbstractDockerCompose object SpringDockerCompose : AbstractDockerCompose( - "app-spring_1", 8080, "docker-compose-spring.yml" + "app-spring_1", 8080, "docker-compose-spring-pg.yml" ) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/docker/AbstractDockerCompose.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/docker/AbstractDockerCompose.kt index 8b2de1c..8182028 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/docker/AbstractDockerCompose.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/fixture/docker/AbstractDockerCompose.kt @@ -15,36 +15,34 @@ private val log = Logger */ abstract class AbstractDockerCompose( private val apps: List, - private val dockerComposeName: String) -: DockerCompose { - - constructor(service: String, port: Int, dockerComposeName: String) - : this(listOf(AppInfo(service, port)), dockerComposeName) - private fun getComposeFile(): File { - val file = File("docker-compose/$dockerComposeName") - if (!file.exists()) throw IllegalArgumentException("file $dockerComposeName not found!") - return file + private val dockerComposeNames: List +) : DockerCompose { + constructor(service: String, port: Int, vararg dockerComposeName: String) + : this(listOf(AppInfo(service, port)), dockerComposeName.toList()) + private fun getComposeFiles(): List = dockerComposeNames.map { + val file = File("docker-compose/$it") + if (!file.exists()) throw IllegalArgumentException("file $it not found!") + file } private val compose = - DockerComposeContainer(getComposeFile()).apply { - withOptions("--compatibility") + DockerComposeContainer(getComposeFiles()).apply { +// withOptions("--compatibility") apps.forEach { (service, port) -> withExposedService( service, port, ) } - withLocalCompose(true) } override fun start() { kotlin.runCatching { compose.start() }.onFailure { - log.e { "Failed to start $dockerComposeName" } + log.e { "Failed to start $dockerComposeNames" } throw it } - log.w("\n=========== $dockerComposeName started =========== \n") + log.w("\n=========== $dockerComposeNames started =========== \n") apps.forEachIndexed { index, _ -> log.i { "Started docker-compose with App at: ${getUrl(index)}" } } @@ -52,7 +50,7 @@ abstract class AbstractDockerCompose( override fun stop() { compose.close() - log.w("\n=========== $dockerComposeName complete =========== \n") + log.w("\n=========== $dockerComposeNames complete =========== \n") } override fun clearDb() { diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccRestTest.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccRestTest.kt index 848a9c6..c76db1b 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccRestTest.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/AccRestTest.kt @@ -4,17 +4,33 @@ import io.kotest.core.annotation.Ignored import ru.otus.otuskotlin.marketplace.e2e.be.docker.WiremockDockerCompose import ru.otus.otuskotlin.marketplace.e2e.be.fixture.BaseFunSpec import ru.otus.otuskotlin.marketplace.blackbox.fixture.docker.DockerCompose +import ru.otus.otuskotlin.marketplace.e2e.be.docker.KtorJvmPGDockerCompose +import ru.otus.otuskotlin.marketplace.e2e.be.docker.KtorLinuxPGDockerCompose +import ru.otus.otuskotlin.marketplace.e2e.be.docker.SpringDockerCompose import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.RestClient +import ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1.toV1 +import ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2.toV2 + +enum class TestDebug { + STUB, PROD, TEST +} // Kotest не сможет подставить правильный аргумент конструктора, поэтому // нужно запретить ему запускать этот класс @Ignored -open class AccRestTestBase(dockerCompose: DockerCompose) : BaseFunSpec(dockerCompose, { +open class AccRestTestBaseFull(dockerCompose: DockerCompose, debug: TestDebug = TestDebug.STUB) : BaseFunSpec(dockerCompose, { val restClient = RestClient(dockerCompose) - testApiV1(restClient, "rest ") - testApiV2(restClient, "rest ") + testApiV1(restClient, prefix = "rest ", debug = debug.toV1()) + testApiV2(restClient, prefix = "rest ", debug = debug.toV2()) }) +@Ignored +open class AccRestTestBaseShort(dockerCompose: DockerCompose, debug: TestDebug = TestDebug.STUB) : BaseFunSpec(dockerCompose, { + val restClient = RestClient(dockerCompose) + testApiV2(restClient, prefix = "rest ", debug = debug.toV2()) +}) + +class AccRestWiremockTest : AccRestTestBaseFull(WiremockDockerCompose) -class AccRestWiremockTest : AccRestTestBase(WiremockDockerCompose) -//class AccRestSpringTest : AccRestTestBase(SpringDockerCompose) -//class AccRestKtorTest : AccRestTestBase(KtorDockerCompose) +class AccRestSpringTest : AccRestTestBaseFull(SpringDockerCompose, debug = TestDebug.PROD) +class AccRestKtorPgJvmTest : AccRestTestBaseFull(KtorJvmPGDockerCompose, debug = TestDebug.PROD) +class AccRestKtorPgLinuxTest : AccRestTestBaseShort(KtorLinuxPGDockerCompose, debug = TestDebug.PROD) diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/createAdV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/createAdV1.kt index fbcc2b6..9eaacd3 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/createAdV1.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/createAdV1.kt @@ -5,10 +5,11 @@ import io.kotest.assertions.withClue import io.kotest.matchers.should import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldMatch import ru.otus.otuskotlin.marketplace.api.v1.models.* import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -suspend fun Client.createAd(ad: AdCreateObject = someCreateAd): AdResponseObject = createAd(ad) { +suspend fun Client.createAd(ad: AdCreateObject = someCreateAd, debug: AdDebug = debugStubV1): AdResponseObject = createAd(ad, debug = debug) { it should haveSuccessResult it.ad shouldNotBe null it.ad?.apply { @@ -16,11 +17,13 @@ suspend fun Client.createAd(ad: AdCreateObject = someCreateAd): AdResponseObject description shouldBe ad.description adType shouldBe ad.adType visibility shouldBe ad.visibility + id.toString() shouldMatch Regex("^[\\d\\w_-]+\$") + lock.toString() shouldMatch Regex("^[\\d\\w_-]+\$") } it.ad!! } -suspend fun Client.createAd(ad: AdCreateObject = someCreateAd, block: (AdCreateResponse) -> T): T = +suspend fun Client.createAd(ad: AdCreateObject = someCreateAd, debug: AdDebug = debugStubV1, block: (AdCreateResponse) -> T): T = withClue("createAdV1: $ad") { val response = sendAndReceive( "ad/create", AdCreateRequest( diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/dataV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/dataV1.kt index 4fd9d6b..c055288 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/dataV1.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/dataV1.kt @@ -1,8 +1,9 @@ package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1 import ru.otus.otuskotlin.marketplace.api.v1.models.* +import ru.otus.otuskotlin.marketplace.e2e.be.test.TestDebug -val debug = AdDebug(mode = AdRequestDebugMode.STUB, stub = AdRequestDebugStubs.SUCCESS) +val debugStubV1 = AdDebug(mode = AdRequestDebugMode.STUB, stub = AdRequestDebugStubs.SUCCESS) val someCreateAd = AdCreateObject( title = "Требуется болт", @@ -10,3 +11,9 @@ val someCreateAd = AdCreateObject( adType = DealSide.DEMAND, visibility = AdVisibility.PUBLIC ) + +fun TestDebug.toV1() = when(this) { + TestDebug.STUB -> debugStubV1 + TestDebug.PROD -> AdDebug(mode = AdRequestDebugMode.PROD) + TestDebug.TEST -> AdDebug(mode = AdRequestDebugMode.TEST) +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/deleteAdV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/deleteAdV1.kt index 14cd2dd..e307c52 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/deleteAdV1.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/deleteAdV1.kt @@ -9,7 +9,7 @@ import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidId import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidLock import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -suspend fun Client.deleteAd(ad: AdResponseObject) { +suspend fun Client.deleteAd(ad: AdResponseObject, debug: AdDebug = debugStubV1) { val id = ad.id val lock = ad.lock withClue("deleteAdV2: $id, lock: $lock") { diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/offersAdV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/offersAdV1.kt index f89a4c5..1d3b981 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/offersAdV1.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/offersAdV1.kt @@ -6,12 +6,12 @@ import io.kotest.matchers.should import ru.otus.otuskotlin.marketplace.api.v1.models.* import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -suspend fun Client.offersAd(id: String?): AdOffersResponse = offersAd(id) { +suspend fun Client.offersAd(id: String?, debug: AdDebug = debugStubV1): AdOffersResponse = offersAd(id, debug = debug) { it should haveSuccessResult it } -suspend fun Client.offersAd(id: String?, block: (AdOffersResponse) -> T): T = +suspend fun Client.offersAd(id: String?, debug: AdDebug = debugStubV1, block: (AdOffersResponse) -> T): T = withClue("searchOffersV1: $id") { val response = sendAndReceive( "ad/offers", diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/readAdV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/readAdV1.kt index 81b0814..cc26621 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/readAdV1.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/readAdV1.kt @@ -4,20 +4,17 @@ import io.kotest.assertions.asClue import io.kotest.assertions.withClue import io.kotest.matchers.should import io.kotest.matchers.shouldNotBe -import ru.otus.otuskotlin.marketplace.api.v1.models.AdReadObject -import ru.otus.otuskotlin.marketplace.api.v1.models.AdReadRequest -import ru.otus.otuskotlin.marketplace.api.v1.models.AdReadResponse -import ru.otus.otuskotlin.marketplace.api.v1.models.AdResponseObject +import ru.otus.otuskotlin.marketplace.api.v1.models.* import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidId import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -suspend fun Client.readAd(id: String?): AdResponseObject = readAd(id) { +suspend fun Client.readAd(id: String?, debug: AdDebug = debugStubV1): AdResponseObject = readAd(id, debug = debug) { it should haveSuccessResult it.ad shouldNotBe null it.ad!! } -suspend fun Client.readAd(id: String?, block: (AdReadResponse) -> T): T = +suspend fun Client.readAd(id: String?, debug: AdDebug = debugStubV1, block: (AdReadResponse) -> T): T = withClue("readAdV1: $id") { id should beValidId diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/searchAdV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/searchAdV1.kt index 3d50359..83ebe0a 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/searchAdV1.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/searchAdV1.kt @@ -3,18 +3,15 @@ package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1 import io.kotest.assertions.asClue import io.kotest.assertions.withClue import io.kotest.matchers.should -import ru.otus.otuskotlin.marketplace.api.v1.models.AdResponseObject -import ru.otus.otuskotlin.marketplace.api.v1.models.AdSearchFilter -import ru.otus.otuskotlin.marketplace.api.v1.models.AdSearchRequest -import ru.otus.otuskotlin.marketplace.api.v1.models.AdSearchResponse +import ru.otus.otuskotlin.marketplace.api.v1.models.* import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -suspend fun Client.searchAd(search: AdSearchFilter): List = searchAd(search) { +suspend fun Client.searchAd(search: AdSearchFilter, debug: AdDebug = debugStubV1): List = searchAd(search, debug = debug) { it should haveSuccessResult it.ads ?: listOf() } -suspend fun Client.searchAd(search: AdSearchFilter, block: (AdSearchResponse) -> T): T = +suspend fun Client.searchAd(search: AdSearchFilter, debug: AdDebug = debugStubV1, block: (AdSearchResponse) -> T): T = withClue("searchAdV1: $search") { val response = sendAndReceive( "ad/search", diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/updateAdV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/updateAdV1.kt index 3eabab4..319248f 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/updateAdV1.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v1/updateAdV1.kt @@ -10,8 +10,8 @@ import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidId import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidLock import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -suspend fun Client.updateAd(ad: AdUpdateObject): AdResponseObject = - updateAd(ad) { +suspend fun Client.updateAd(ad: AdUpdateObject, debug: AdDebug = debugStubV1): AdResponseObject = + updateAd(ad, debug = debug) { it should haveSuccessResult it.ad shouldNotBe null it.ad?.apply { @@ -27,7 +27,7 @@ suspend fun Client.updateAd(ad: AdUpdateObject): AdResponseObject = it.ad!! } -suspend fun Client.updateAd(ad: AdUpdateObject, block: (AdUpdateResponse) -> T): T { +suspend fun Client.updateAd(ad: AdUpdateObject, debug: AdDebug = debugStubV1, block: (AdUpdateResponse) -> T): T { val id = ad.id val lock = ad.lock return withClue("updatedV1: $id, lock: $lock, set: $ad") { diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/createAdV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/createAdV2.kt index eea62fc..0e5132a 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/createAdV2.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/createAdV2.kt @@ -5,10 +5,11 @@ import io.kotest.assertions.withClue import io.kotest.matchers.should import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe +import io.kotest.matchers.string.shouldMatch import ru.otus.otuskotlin.marketplace.api.v2.models.* import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -suspend fun Client.createAd(ad: AdCreateObject = someCreateAd): AdResponseObject = createAd(ad) { +suspend fun Client.createAd(ad: AdCreateObject = someCreateAd, debug: AdDebug = debugStubV2): AdResponseObject = createAd(ad, debug = debug) { it should haveSuccessResult it.ad shouldNotBe null it.ad?.apply { @@ -16,18 +17,20 @@ suspend fun Client.createAd(ad: AdCreateObject = someCreateAd): AdResponseObject description shouldBe ad.description adType shouldBe ad.adType visibility shouldBe ad.visibility + id.toString() shouldMatch Regex("^[\\d\\w_-]+\$") + lock.toString() shouldMatch Regex("^[\\d\\w_-]+\$") } it.ad!! } -suspend fun Client.createAd(ad: AdCreateObject = someCreateAd, block: (AdCreateResponse) -> T): T = +suspend fun Client.createAd(ad: AdCreateObject = someCreateAd, debug: AdDebug = debugStubV2, block: (AdCreateResponse) -> T): T = withClue("createAdV2: $ad") { - val response = sendAndReceive( + val response: AdCreateResponse = sendAndReceive( "ad/create", AdCreateRequest( debug = debug, ad = ad ) - ) as AdCreateResponse + ) response.asClue(block) } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/dataV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/dataV2.kt index 88f3c75..8de702e 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/dataV2.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/dataV2.kt @@ -1,9 +1,9 @@ package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 import ru.otus.otuskotlin.marketplace.api.v2.models.* +import ru.otus.otuskotlin.marketplace.e2e.be.test.TestDebug - -val debug = AdDebug(mode = AdRequestDebugMode.STUB, stub = AdRequestDebugStubs.SUCCESS) +val debugStubV2: AdDebug = AdDebug(mode = AdRequestDebugMode.STUB, stub = AdRequestDebugStubs.SUCCESS) val someCreateAd = AdCreateObject( title = "Требуется болт", @@ -11,3 +11,9 @@ val someCreateAd = AdCreateObject( adType = DealSide.DEMAND, visibility = AdVisibility.PUBLIC ) + +fun TestDebug.toV2() = when(this) { + TestDebug.STUB -> debugStubV2 + TestDebug.PROD -> AdDebug(mode = AdRequestDebugMode.PROD) + TestDebug.TEST -> AdDebug(mode = AdRequestDebugMode.TEST) +} diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/deleteAdV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/deleteAdV2.kt index c5e72fa..1882695 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/deleteAdV2.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/deleteAdV2.kt @@ -4,28 +4,25 @@ import io.kotest.assertions.asClue import io.kotest.assertions.withClue import io.kotest.matchers.should import io.kotest.matchers.shouldBe -import ru.otus.otuskotlin.marketplace.api.v2.models.AdDeleteObject -import ru.otus.otuskotlin.marketplace.api.v2.models.AdDeleteRequest -import ru.otus.otuskotlin.marketplace.api.v2.models.AdDeleteResponse -import ru.otus.otuskotlin.marketplace.api.v2.models.AdResponseObject +import ru.otus.otuskotlin.marketplace.api.v2.models.* import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidId import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidLock import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -suspend fun Client.deleteAd(ad: AdResponseObject) { +suspend fun Client.deleteAd(ad: AdResponseObject, debug: AdDebug = debugStubV2) { val id = ad.id val lock = ad.lock withClue("deleteAdV2: $id, lock: $lock") { id should beValidId lock should beValidLock - val response = sendAndReceive( + val response: AdDeleteResponse = sendAndReceive( "ad/delete", AdDeleteRequest( debug = debug, ad = AdDeleteObject(id = id, lock = lock) ) - ) as AdDeleteResponse + ) response.asClue { response should haveSuccessResult diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/offersAdV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/offersAdV2.kt index 8a42289..709a9b1 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/offersAdV2.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/offersAdV2.kt @@ -3,23 +3,26 @@ package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 import io.kotest.assertions.asClue import io.kotest.assertions.withClue import io.kotest.matchers.should -import ru.otus.otuskotlin.marketplace.api.v2.models.* +import ru.otus.otuskotlin.marketplace.api.v2.models.AdDebug +import ru.otus.otuskotlin.marketplace.api.v2.models.AdOffersRequest +import ru.otus.otuskotlin.marketplace.api.v2.models.AdOffersResponse +import ru.otus.otuskotlin.marketplace.api.v2.models.AdReadObject import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -suspend fun Client.offersAd(id: String?): AdOffersResponse = offersAd(id) { +suspend fun Client.offersAd(id: String?, debug: AdDebug = debugStubV2): AdOffersResponse = offersAd(id, debug = debug) { it should haveSuccessResult it } -suspend fun Client.offersAd(id: String?, block: (AdOffersResponse) -> T): T = +suspend fun Client.offersAd(id: String?, debug: AdDebug = debugStubV2, block: (AdOffersResponse) -> T): T = withClue("searchOffersV2: $id") { - val response = sendAndReceive( + val response: AdOffersResponse = sendAndReceive( "ad/offers", AdOffersRequest( debug = debug, ad = AdReadObject(id = id), ) - ) as AdOffersResponse + ) response.asClue(block) } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/readAdV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/readAdV2.kt index b450862..d72b79b 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/readAdV2.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/readAdV2.kt @@ -4,30 +4,27 @@ import io.kotest.assertions.asClue import io.kotest.assertions.withClue import io.kotest.matchers.should import io.kotest.matchers.shouldNotBe -import ru.otus.otuskotlin.marketplace.api.v2.models.AdReadObject -import ru.otus.otuskotlin.marketplace.api.v2.models.AdReadRequest -import ru.otus.otuskotlin.marketplace.api.v2.models.AdReadResponse -import ru.otus.otuskotlin.marketplace.api.v2.models.AdResponseObject +import ru.otus.otuskotlin.marketplace.api.v2.models.* import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidId import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -suspend fun Client.readAd(id: String?): AdResponseObject = readAd(id) { +suspend fun Client.readAd(id: String?, debug: AdDebug = debugStubV2): AdResponseObject = readAd(id, debug = debug) { it should haveSuccessResult it.ad shouldNotBe null it.ad!! } -suspend fun Client.readAd(id: String?, block: (AdReadResponse) -> T): T = +suspend fun Client.readAd(id: String?, debug: AdDebug = debugStubV2, block: (AdReadResponse) -> T): T = withClue("readAdV1: $id") { id should beValidId - val response = sendAndReceive( + val response: AdReadResponse = sendAndReceive( "ad/read", AdReadRequest( debug = debug, ad = AdReadObject(id = id) ) - ) as AdReadResponse + ) response.asClue(block) } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/searchAdV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/searchAdV2.kt index 32a2283..1d684f5 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/searchAdV2.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/searchAdV2.kt @@ -3,26 +3,23 @@ package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 import io.kotest.assertions.asClue import io.kotest.assertions.withClue import io.kotest.matchers.should -import ru.otus.otuskotlin.marketplace.api.v2.models.AdResponseObject -import ru.otus.otuskotlin.marketplace.api.v2.models.AdSearchFilter -import ru.otus.otuskotlin.marketplace.api.v2.models.AdSearchRequest -import ru.otus.otuskotlin.marketplace.api.v2.models.AdSearchResponse +import ru.otus.otuskotlin.marketplace.api.v2.models.* import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -suspend fun Client.searchAd(search: AdSearchFilter): List = searchAd(search) { +suspend fun Client.searchAd(search: AdSearchFilter, debug: AdDebug = debugStubV2): List = searchAd(search, debug = debug) { it should haveSuccessResult it.ads ?: listOf() } -suspend fun Client.searchAd(search: AdSearchFilter, block: (AdSearchResponse) -> T): T = +suspend fun Client.searchAd(search: AdSearchFilter, debug: AdDebug = debugStubV2, block: (AdSearchResponse) -> T): T = withClue("searchAdV2: $search") { - val response = sendAndReceive( + val response: AdSearchResponse = sendAndReceive( "ad/search", AdSearchRequest( debug = debug, adFilter = search, ) - ) as AdSearchResponse + ) response.asClue(block) } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/sendAndReceiveV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/sendAndReceiveV2.kt index 9bfd262..8efcdc5 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/sendAndReceiveV2.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/sendAndReceiveV2.kt @@ -1,20 +1,22 @@ package ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2 import co.touchlab.kermit.Logger -import ru.otus.otuskotlin.marketplace.api.v2.apiV2RequestSerialize -import ru.otus.otuskotlin.marketplace.api.v2.apiV2ResponseDeserialize +import ru.otus.otuskotlin.marketplace.api.v2.apiV2RequestSimpleSerialize +import ru.otus.otuskotlin.marketplace.api.v2.apiV2ResponseSimpleDeserialize import ru.otus.otuskotlin.marketplace.api.v2.models.IRequest import ru.otus.otuskotlin.marketplace.api.v2.models.IResponse import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -private val log = Logger -suspend fun Client.sendAndReceive(path: String, request: IRequest): IResponse { - val requestBody = apiV2RequestSerialize(request) +suspend inline fun Client.sendAndReceive(path: String, request: Rq): Rs { + val log = Logger + val requestBody = apiV2RequestSimpleSerialize(request) +// val requestBody = apiV2RequestSerialize(request) log.w { "Send to v2/$path\n$requestBody" } val responseBody = sendAndReceive("v2", path, requestBody) log.w { "Received\n$responseBody" } - return apiV2ResponseDeserialize(responseBody) +// return apiV2ResponseDeserialize(responseBody) + return apiV2ResponseSimpleDeserialize(responseBody) } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/updateAdV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/updateAdV2.kt index f958aaa..0d42d22 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/updateAdV2.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/action/v2/updateAdV2.kt @@ -10,13 +10,13 @@ import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidId import ru.otus.otuskotlin.marketplace.blackbox.test.action.beValidLock import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client -suspend fun Client.updateAd(ad: AdUpdateObject): AdResponseObject = - updateAd(ad) { +suspend fun Client.updateAd(ad: AdUpdateObject, debug: AdDebug = debugStubV2): AdResponseObject = + updateAd(ad, debug = debug) { it should haveSuccessResult it.ad shouldNotBe null - it.ad?.apply { - if (ad.title != null) - title shouldBe ad.title + it.ad!!.apply { +// if (ad.title != null) + title shouldBe ad.title if (ad.description != null) description shouldBe ad.description if (ad.adType != null) @@ -24,22 +24,22 @@ suspend fun Client.updateAd(ad: AdUpdateObject): AdResponseObject = if (ad.visibility != null) visibility shouldBe ad.visibility } - it.ad!! +// it.ad!! } -suspend fun Client.updateAd(ad: AdUpdateObject, block: (AdUpdateResponse) -> T): T { +suspend fun Client.updateAd(ad: AdUpdateObject, debug: AdDebug = debugStubV2, block: (AdUpdateResponse) -> T): T { val id = ad.id val lock = ad.lock return withClue("updatedV1: $id, lock: $lock, set: $ad") { id should beValidId lock should beValidLock - val response = sendAndReceive( + val response: AdUpdateResponse = sendAndReceive( "ad/update", AdUpdateRequest( debug = debug, ad = ad.copy(id = id, lock = lock) ) - ) as AdUpdateResponse + ) response.asClue(block) } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV1.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV1.kt index acdc0eb..f26d92c 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV1.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV1.kt @@ -6,29 +6,29 @@ import io.kotest.assertions.withClue import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.collections.shouldExist import io.kotest.matchers.collections.shouldExistInOrder -import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe +import ru.otus.otuskotlin.marketplace.api.v1.models.AdDebug import ru.otus.otuskotlin.marketplace.api.v1.models.AdSearchFilter import ru.otus.otuskotlin.marketplace.api.v1.models.AdUpdateObject import ru.otus.otuskotlin.marketplace.api.v1.models.DealSide import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client import ru.otus.otuskotlin.marketplace.e2e.be.test.action.v1.* -fun FunSpec.testApiV1(client: Client, prefix: String = "") { +fun FunSpec.testApiV1(client: Client, prefix: String = "", debug: AdDebug = debugStubV1) { context("${prefix}v1") { test("Create Ad ok") { - client.createAd() + client.createAd(debug = debug) } test("Read Ad ok") { - val created = client.createAd() - client.readAd(created.id).asClue { + val created = client.createAd(debug = debug) + client.readAd(created.id, debug = debug).asClue { it shouldBe created } } test("Update Ad ok") { - val created = client.createAd() + val created = client.createAd(debug = debug) val updateAd = AdUpdateObject( id = created.id, lock = created.lock, @@ -37,40 +37,39 @@ fun FunSpec.testApiV1(client: Client, prefix: String = "") { adType = created.adType, visibility = created.visibility, ) - client.updateAd(updateAd) + client.updateAd(updateAd, debug = debug) } test("Delete Ad ok") { - val created = client.createAd() - client.deleteAd(created) + val created = client.createAd(debug = debug) + client.deleteAd(created, debug = debug) // client.readAd(created.id) { // it should haveError("not-found") // } } test("Search Ad ok") { - val created1 = client.createAd(someCreateAd.copy(title = "Selling Bolt")) - val created2 = client.createAd(someCreateAd.copy(title = "Selling Nut")) + val created1 = client.createAd(someCreateAd.copy(title = "Selling Bolt"), debug = debug) + val created2 = client.createAd(someCreateAd.copy(title = "Selling Nut"), debug = debug) withClue("Search Selling") { - val results = client.searchAd(search = AdSearchFilter(searchString = "Selling")) - results shouldHaveSize 2 + val results = client.searchAd(search = AdSearchFilter(searchString = "Selling"), debug = debug) results shouldExist { it.title == created1.title } results shouldExist { it.title == created2.title } } withClue("Search Bolt") { - client.searchAd(search = AdSearchFilter(searchString = "Bolt")) + client.searchAd(search = AdSearchFilter(searchString = "Bolt"), debug = debug) .shouldExistInOrder({ it.title == created1.title }) } } test("Offer Ad ok") { - val supply = client.createAd(someCreateAd.copy(title = "Some Bolt", adType = DealSide.SUPPLY)) - val demand = client.createAd(someCreateAd.copy(title = "Some Bolt", adType = DealSide.DEMAND)) + val supply = client.createAd(someCreateAd.copy(title = "Some Bolt", adType = DealSide.SUPPLY), debug = debug) + val demand = client.createAd(someCreateAd.copy(title = "Some Bolt", adType = DealSide.DEMAND), debug = debug) withClue("Find offer for supply") { - val res1 = client.offersAd(supply.id) + val res1 = client.offersAd(supply.id, debug = debug) res1.ad?.adType shouldBe supply.adType res1.ads?.shouldExistInOrder({ it.adType == demand.adType }) ?: fail("Empty ads") } diff --git a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV2.kt b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV2.kt index 6f548f3..84e7e31 100644 --- a/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV2.kt +++ b/ok-marketplace-tests/ok-marketplace-e2e-be/src/test/kotlin/test/testApiV2.kt @@ -6,29 +6,29 @@ import io.kotest.assertions.withClue import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.collections.shouldExist import io.kotest.matchers.collections.shouldExistInOrder -import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe +import ru.otus.otuskotlin.marketplace.api.v2.models.AdDebug import ru.otus.otuskotlin.marketplace.api.v2.models.AdSearchFilter import ru.otus.otuskotlin.marketplace.api.v2.models.AdUpdateObject import ru.otus.otuskotlin.marketplace.api.v2.models.DealSide import ru.otus.otuskotlin.marketplace.e2e.be.fixture.client.Client import ru.otus.otuskotlin.marketplace.e2e.be.test.action.v2.* -fun FunSpec.testApiV2(client: Client, prefix: String = "") { +fun FunSpec.testApiV2(client: Client, prefix: String = "", debug: AdDebug = debugStubV2) { context("${prefix}v2") { test("Create Ad ok") { - client.createAd() + client.createAd(debug = debug) } test("Read Ad ok") { - val created = client.createAd() - client.readAd(created.id).asClue { + val created = client.createAd(debug = debug) + client.readAd(created.id, debug = debug).asClue { it shouldBe created } } test("Update Ad ok") { - val created = client.createAd() + val created = client.createAd(debug = debug) val updateAd = AdUpdateObject( id = created.id, lock = created.lock, @@ -38,44 +38,42 @@ fun FunSpec.testApiV2(client: Client, prefix: String = "") { visibility = created.visibility, productId = created.productId, ) - client.updateAd(updateAd) + client.updateAd(updateAd, debug = debug) } test("Delete Ad ok") { - val created = client.createAd() - client.deleteAd(created) + val created = client.createAd(debug = debug) + client.deleteAd(created, debug = debug) // client.readAd(created.id) { // it should haveError("not-found") // } } test("Search Ad ok") { - val created1 = client.createAd(someCreateAd.copy(title = "Selling Bolt")) - val created2 = client.createAd(someCreateAd.copy(title = "Selling Nut")) + val created1 = client.createAd(someCreateAd.copy(title = "Selling Bolt"), debug = debug) + val created2 = client.createAd(someCreateAd.copy(title = "Selling Nut"), debug = debug) withClue("Search Selling") { - val results = client.searchAd(search = AdSearchFilter(searchString = "Selling")) - results shouldHaveSize 2 + val results = client.searchAd(search = AdSearchFilter(searchString = "Selling"), debug = debug) results shouldExist { it.title == created1.title } results shouldExist { it.title == created2.title } } withClue("Search Bolt") { - client.searchAd(search = AdSearchFilter(searchString = "Bolt")) + client.searchAd(search = AdSearchFilter(searchString = "Bolt"), debug = debug) .shouldExistInOrder({ it.title == created1.title }) } } test("Offer Ad ok") { - val supply = client.createAd(someCreateAd.copy(title = "Some Bolt", adType = DealSide.SUPPLY)) - val demand = client.createAd(someCreateAd.copy(title = "Some Bolt", adType = DealSide.DEMAND)) + val supply = client.createAd(someCreateAd.copy(title = "Some Bolt", adType = DealSide.SUPPLY), debug = debug) + val demand = client.createAd(someCreateAd.copy(title = "Some Bolt", adType = DealSide.DEMAND), debug = debug) withClue("Find offer for supply") { - val res1 = client.offersAd(supply.id) + val res1 = client.offersAd(supply.id, debug = debug) res1.ad?.adType shouldBe supply.adType res1.ads?.shouldExistInOrder({ it.adType == demand.adType }) ?: fail("Empty ads") } } } - } diff --git a/pgkn b/pgkn new file mode 160000 index 0000000..ac0c93e --- /dev/null +++ b/pgkn @@ -0,0 +1 @@ +Subproject commit ac0c93ed52dabc20c8a4a81f0675f6962f2c2ae1 diff --git a/settings.gradle.kts b/settings.gradle.kts index 178493a..1e51cad 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,3 +15,4 @@ includeBuild("ok-marketplace-be") includeBuild("ok-marketplace-libs") includeBuild("ok-marketplace-tests") +includeBuild("pgkn") \ No newline at end of file