diff --git a/build.gradle b/build.gradle index a9212305a..308678224 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,9 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { - ext.kotlin_version = '1.9.10' - ext.ktor_version = '2.3.7' - ext.exposed_version = '0.41.1' + ext.kotlin_version = '1.9.22' ext.klogging_version = '3.0.4' ext.jacksonKt_version = '2.14.1' - ext.libVersion = '2024.0.3' + ext.libVersion = '2024.0.4' repositories { mavenCentral() maven { @@ -13,7 +12,7 @@ buildscript { } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.1" + classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.5" classpath "org.jlleitschuh.gradle:ktlint-gradle:11.6.1" } } @@ -52,22 +51,15 @@ subprojects { group = 'io.github.adven27' version = libVersion - sourceCompatibility = 11 - targetCompatibility = 11 + sourceCompatibility = 17 + targetCompatibility = 17 compileJava.options.encoding = 'utf-8' compileTestJava.options.encoding = 'utf-8' - compileKotlin { + tasks.withType(KotlinCompile) { kotlinOptions { - freeCompilerArgs = ['-Xjsr305=strict'] - jvmTarget = '11' - } - } - - compileTestKotlin { - kotlinOptions { - freeCompilerArgs = ['-Xjsr305=strict'] - jvmTarget = '11' + freeCompilerArgs += '-Xjsr305=strict' + jvmTarget = '17' } } diff --git a/exam-core/src/main/java/io/github/adven27/concordion/extensions/exam/core/Content.kt b/exam-core/src/main/java/io/github/adven27/concordion/extensions/exam/core/Content.kt index b21366c05..aee57e70e 100644 --- a/exam-core/src/main/java/io/github/adven27/concordion/extensions/exam/core/Content.kt +++ b/exam-core/src/main/java/io/github/adven27/concordion/extensions/exam/core/Content.kt @@ -43,6 +43,8 @@ open class Content(val body: String, val type: String) { class Text(content: String) : Content(content, "text") fun pretty() = body.pretty(type) + + override fun toString() = pretty() } interface ContentPrinter { diff --git a/exam-core/src/main/java/io/github/adven27/concordion/extensions/exam/core/ExamExtension.kt b/exam-core/src/main/java/io/github/adven27/concordion/extensions/exam/core/ExamExtension.kt index 1a456249b..e3388b2f5 100644 --- a/exam-core/src/main/java/io/github/adven27/concordion/extensions/exam/core/ExamExtension.kt +++ b/exam-core/src/main/java/io/github/adven27/concordion/extensions/exam/core/ExamExtension.kt @@ -176,12 +176,20 @@ class ExamExtension(private vararg var plugins: ExamPlugin) : ConcordionExtensio }.first } + const val VERIFIER_JSON = "json" + const val VERIFIER_JSON_IGNORE_EXTRA_FIELDS = "jsonIgnoreExtraFields" + const val VERIFIER_JSON_ARRAY_ORDERED = "jsonArrayOrdered" + const val VERIFIER_XML = "xml" + const val VERIFIER_TEXT = "text" + val CONTENT_VERIFIERS: MutableMap = mutableMapOf( - "json" to JsonVerifier(), - "jsonIgnoreExtraFields" to JsonVerifier { it.withOptions(IGNORING_EXTRA_FIELDS) }, - "jsonArrayOrdered" to JsonVerifier { it.withOptions(it.options.apply { remove(IGNORING_ARRAY_ORDER) }) }, - "xml" to XmlVerifier(), - "text" to ContentVerifier.Default("text") + VERIFIER_JSON to JsonVerifier(), + VERIFIER_JSON_IGNORE_EXTRA_FIELDS to JsonVerifier { it.withOptions(IGNORING_EXTRA_FIELDS) }, + VERIFIER_JSON_ARRAY_ORDERED to JsonVerifier { + it.withOptions(it.options.apply { remove(IGNORING_ARRAY_ORDER) }) + }, + VERIFIER_XML to XmlVerifier(), + VERIFIER_TEXT to ContentVerifier.Default("text") ) @JvmStatic diff --git a/exam-core/src/main/java/io/github/adven27/concordion/extensions/exam/core/handlebars/matchers/MatcherHelpers.kt b/exam-core/src/main/java/io/github/adven27/concordion/extensions/exam/core/handlebars/matchers/MatcherHelpers.kt index a9bdbbcd3..baacceeb5 100644 --- a/exam-core/src/main/java/io/github/adven27/concordion/extensions/exam/core/handlebars/matchers/MatcherHelpers.kt +++ b/exam-core/src/main/java/io/github/adven27/concordion/extensions/exam/core/handlebars/matchers/MatcherHelpers.kt @@ -1,20 +1,16 @@ package io.github.adven27.concordion.extensions.exam.core.handlebars.matchers -import com.github.jknack.handlebars.Context import com.github.jknack.handlebars.Options import io.github.adven27.concordion.extensions.exam.core.handlebars.ExamHelper import io.github.adven27.concordion.extensions.exam.core.utils.DateFormattedAndWithin.Companion.PARAMS_SEPARATOR import io.github.adven27.concordion.extensions.exam.core.utils.parseDate import io.github.adven27.concordion.extensions.exam.core.utils.toLocalDate import io.github.adven27.concordion.extensions.exam.core.utils.toLocalDateTime -import org.concordion.api.Evaluator import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.* -import java.util.regex.Pattern const val PLACEHOLDER_TYPE = "placeholder_type" -const val DB_ACTUAL = "db_actual" const val ISO_LOCAL_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss" const val ISO_LOCAL_DATE_FORMAT = "yyyy-MM-dd" @@ -80,11 +76,7 @@ enum class MatcherHelpers( expected = "\${json-unit.regex}\\d+" ) { override fun invoke(context: Any?, options: Options): Any = - if (placeholderType(options.context) == "db") { - regexMatches(context.toString(), dbActual(options.context)) - } else { - "\${${placeholderType(options.context)}-unit.regex}$context" - } + "\${${placeholderType(options.context)}-unit.regex}$context" }, after( example = "{{after (today)}}", @@ -175,8 +167,6 @@ enum class MatcherHelpers( "\${${placeholderType(options.context)}-unit.matches:$context}${options.param(0, "")}" }; - protected fun dbActual(context: Context) = (context.model() as Evaluator).getVariable("#$DB_ACTUAL") - override fun apply(context: Any?, options: Options): Any? { validate(options) val result = try { @@ -190,6 +180,3 @@ enum class MatcherHelpers( override fun toString() = this.describe() abstract operator fun invoke(context: Any?, options: Options): Any? } - -private fun regexMatches(p: String, value: Any?): Boolean = - value.takeIf { it != null }?.let { Pattern.compile(p).matcher(it.toString()).matches() } ?: false diff --git a/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/DbPlugin.kt b/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/DbPlugin.kt index 0ccf07eba..6c1a97361 100644 --- a/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/DbPlugin.kt +++ b/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/DbPlugin.kt @@ -1,7 +1,9 @@ package io.github.adven27.concordion.extensions.exam.db import com.github.jknack.handlebars.Options +import io.github.adven27.concordion.extensions.exam.core.Content import io.github.adven27.concordion.extensions.exam.core.ExamPlugin +import io.github.adven27.concordion.extensions.exam.core.html.pre import io.github.adven27.concordion.extensions.exam.core.html.span import io.github.adven27.concordion.extensions.exam.db.commands.DbCleanCommand import io.github.adven27.concordion.extensions.exam.db.commands.DbExecuteCommand @@ -107,19 +109,21 @@ class DbPlugin @JvmOverloads constructor( interface ValuePrinter { open class Default @JvmOverloads constructor( formatter: DateTimeFormatter = ISO_LOCAL_DATE_TIME, - private val tableColumnType: Map = mapOf() + private val tableColumnStyle: Map = mapOf() ) : AbstractDefault(formatter) { data class TableColumn(val table: String, val column: String) override fun orElse(value: Any): String = value.toString() override fun wrap(table: String, column: String, value: Any?): Element = - tableColumnType.entries - .filter { (tc, _) -> tc.eq(table, column) } - .map { it.value } - .firstOrNull() - ?.let { Element("pre").addStyleClass(it).appendText(print(table, column, value)) } - ?: super.wrap(table, column, value) + super.wrap(table, column, value).let { e -> + tableColumnStyle.entries + .filter { (tc, _) -> tc.eq(table, column) } + .map { it.value } + .firstOrNull() + ?.let { e.addStyleClass(it) } + ?: e + } private fun TableColumn.eq(t: String, c: String) = table.equals(t, ignoreCase = true) && column.equals(c, ignoreCase = true) @@ -131,12 +135,16 @@ class DbPlugin @JvmOverloads constructor( is Array<*> -> value.contentToString() is java.sql.Date -> printDate(Date(value.time)) is Date -> printDate(value) + is Content -> value.pretty() else -> orElse(value) } private fun printDate(value: Date) = formatter.withZone(ZoneId.systemDefault()).format(value.toInstant()) override fun wrap(table: String, column: String, value: Any?): Element = - span(print(table, column, value)).el + when (value) { + is Content -> pre(print(table, column, value), "class" to value.type).el + else -> span(print(table, column, value)).el + } abstract fun orElse(value: Any): String } diff --git a/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/DbTester.kt b/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/DbTester.kt index 0b86727e0..4bf065cc8 100644 --- a/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/DbTester.kt +++ b/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/DbTester.kt @@ -1,5 +1,6 @@ package io.github.adven27.concordion.extensions.exam.db +import io.github.adven27.concordion.extensions.exam.core.Content import io.github.adven27.concordion.extensions.exam.core.commands.AwaitConfig import io.github.adven27.concordion.extensions.exam.core.commands.Verifier import io.github.adven27.concordion.extensions.exam.core.html.fileExt @@ -8,6 +9,7 @@ import io.github.adven27.concordion.extensions.exam.db.builder.CompareOperation. import io.github.adven27.concordion.extensions.exam.db.builder.ContainsFilterTable import io.github.adven27.concordion.extensions.exam.db.builder.DataBaseSeedingException import io.github.adven27.concordion.extensions.exam.db.builder.ExamDataSet +import io.github.adven27.concordion.extensions.exam.db.builder.ExamTable import io.github.adven27.concordion.extensions.exam.db.builder.JSONDataSet import io.github.adven27.concordion.extensions.exam.db.builder.SeedStrategy import io.github.adven27.concordion.extensions.exam.db.builder.SeedStrategy.CLEAN_INSERT @@ -80,6 +82,19 @@ open class DbTester @JvmOverloads constructor( strategy.operation.execute(connection(ds), ExamDataSet(table, eval)) } + fun metaData(seed: TableSeed) = TableSeed( + seed.ds, + ExamTable( + CompositeTable( + connection(seed.ds) + .createTable(seed.table.tableName()).withColumnsAsIn(seed.table).tableMetaData, + (seed.table as ExamTable).delegate + ), + seed.table.eval + ), + seed.strategy + ) + fun seed(seed: FilesSeed, eval: Evaluator) = requireAllowedStrategy(seed).let { try { order(loadDataSet(eval, it.datasets), it.tableOrdering).apply { @@ -488,7 +503,7 @@ open class DbTesterBase @JvmOverloads constructor( createQueryTable(tableName, "SELECT * FROM $tableName WHERE $filter") fun actualWithDependentTables(ds: String?, table: String): IDataSet = connection(ds).let { - it.createDataSet(getAllDependentTables(it, QualifiedTableName(table.uppercase(), it.schema).qualifiedName)) + it.createDataSet(getAllDependentTables(it, QualifiedTableName(table, it.schema).qualifiedName)) } protected fun actualDataSet(tables: Array): IDataSet = connection.createDataSet(tables) @@ -570,13 +585,18 @@ private fun List.prettyPrint(): String = .toSortedMap().entries.joinToString("\n") { """${it.key}: ${it.value}""" } class JsonbPostgresqlDataTypeFactory : PostgresqlDataTypeFactory() { - override fun createDataType(sqlType: Int, sqlTypeName: String?): DataType = - if (sqlTypeName == "jsonb") JsonbDataType() else super.createDataType(sqlType, sqlTypeName) + override fun createDataType(sqlType: Int, sqlTypeName: String?): DataType = when (sqlTypeName) { + in listOf("jsonb", "json") -> JsonbDataType(sqlTypeName!!) + else -> super.createDataType(sqlType, sqlTypeName) + } - class JsonbDataType : AbstractDataType("jsonb", Types.OTHER, String::class.java, false) { - override fun typeCast(obj: Any?): Any = obj.toString() + class JsonbDataType(name: String) : AbstractDataType(name, Types.OTHER, Content.Json::class.java, false) { + override fun typeCast(obj: Any?): Content.Json? = obj?.let { + if (it is Content.Json) it else Content.Json(it.toString()) + } - override fun getSqlValue(column: Int, resultSet: ResultSet): Any? = resultSet.getString(column) + override fun getSqlValue(column: Int, resultSet: ResultSet): Any? = + resultSet.getString(column)?.let { Content.Json(it) } override fun setSqlValue(value: Any?, column: Int, statement: PreparedStatement) = statement.setObject( column, diff --git a/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/builder/DataSet.kt b/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/builder/DataSet.kt index feda9c1a7..ee8f47706 100644 --- a/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/builder/DataSet.kt +++ b/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/builder/DataSet.kt @@ -144,17 +144,18 @@ class ExamDataSetIterator(private val delegate: ITableIterator, private val eval override fun getTable(): ITable = ExamTable(delegate.table, eval) } -class ExamTable(private val delegate: ITable, private val eval: Evaluator) : ITable { +class ExamTable(val delegate: ITable, val eval: Evaluator) : ITable { override fun getTableMetaData(): ITableMetaData = delegate.tableMetaData override fun getRowCount(): Int = delegate.rowCount @Throws(DataSetException::class) override fun getValue(row: Int, column: String): Any? { val value = delegate.getValue(row, column) + val dataType = tableMetaData.columns.first { it.columnName.equals(column, ignoreCase = true) }.dataType return try { when { value is String && value.isRange() -> value.toRange().toList().let { it[row % it.size] } - value is String && !value.startsWith("!{") -> eval.resolveToObj(value) + value is String -> dataType.typeCast(eval.resolveToObj(value)) else -> value } } catch (expected: Throwable) { diff --git a/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/commands/Comparers.kt b/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/commands/Comparers.kt index 15af34da2..becae528c 100644 --- a/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/commands/Comparers.kt +++ b/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/commands/Comparers.kt @@ -1,5 +1,6 @@ package io.github.adven27.concordion.extensions.exam.db.commands +import io.github.adven27.concordion.extensions.exam.core.Content import io.github.adven27.concordion.extensions.exam.core.ContentVerifier import io.github.adven27.concordion.extensions.exam.core.ContentVerifier.Companion.setActualIfNeeded import io.github.adven27.concordion.extensions.exam.core.ExamExtension.Companion.contentVerifier @@ -16,6 +17,7 @@ import java.util.* open class ExamMatchersAwareValueComparer : IsActualEqualToExpectedValueComparer() { protected lateinit var evaluator: Evaluator + protected var error = "" fun setEvaluator(evaluator: Evaluator): ExamMatchersAwareValueComparer { this.evaluator = evaluator @@ -30,15 +32,29 @@ open class ExamMatchersAwareValueComparer : IsActualEqualToExpectedValueComparer dataType: DataType, expected: Any?, actual: Any? - ): Boolean = when { - expected.isError() -> false - expected.isMatcher() -> setActualIfNeeded(expected as String, actual, evaluator).let { - ContentVerifier.matcher(it, "\${test-unit.").matches(actual) - } + ): Boolean { + error = "" + return when { + expected.isError() -> false + expected.isMatcher() -> setActualIfNeeded(expected as String, actual, evaluator).let { + ContentVerifier.matcher(it, "\${test-unit.").matches(actual) + } + + actual is Content && expected is Content -> verify(actual, expected.body) + actual is Content && expected is String -> verify(actual, expected) - else -> super.isExpected(expectedTable, actualTable, rowNum, columnName, dataType, expected, actual) + else -> super.isExpected(expectedTable, actualTable, rowNum, columnName, dataType, expected, actual) + } } + private fun verify(actual: Content, expected: String) = + contentVerifier(actual.type).verify(expected, actual.body, evaluator) + .onFailure { error = " because:\n" + it.rootCauseMessage() } + .isSuccess + + override fun makeFailMessage(expectedValue: Any?, actualValue: Any?): String = + super.makeFailMessage(expectedValue, actualValue) + error + companion object { @JvmField var ERROR_MARKER = "ERROR RETRIEVING VALUE: " @@ -84,8 +100,7 @@ fun sortedTable(table: ITable, columns: Array, rowComparator: RowCompara setRowComparator(rowComparator.init(table, columns)) } -open class TypedColumnComparer(val type: String) : ExamMatchersAwareValueComparer() { - private var error = "" +open class VerifierColumnComparer(private val verifier: String) : ExamMatchersAwareValueComparer() { override fun isExpected( expectedTable: ITable?, actualTable: ITable?, @@ -94,14 +109,11 @@ open class TypedColumnComparer(val type: String) : ExamMatchersAwareValueCompare dataType: DataType, expected: Any?, actual: Any? - ) = contentVerifier(type).verify(expected.toString(), actual.toString(), evaluator) + ) = contentVerifier(verifier).verify(expected.toString(), actual.toString(), evaluator) .onFailure { error = " because:\n" + it.rootCauseMessage() } .onSuccess { error = "" } .isSuccess - - override fun makeFailMessage(expectedValue: Any?, actualValue: Any?): String = - super.makeFailMessage(expectedValue, actualValue) + error } @Suppress("unused") -class JsonColumnComparer : TypedColumnComparer("json") +class JsonColumnComparer : VerifierColumnComparer("json") diff --git a/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/commands/DbSetCommand.kt b/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/commands/DbSetCommand.kt index d560dd5b1..c506fb635 100644 --- a/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/commands/DbSetCommand.kt +++ b/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/commands/DbSetCommand.kt @@ -20,7 +20,7 @@ open class DbSetCommand( override fun render(commandCall: CommandCall, result: Model) = renderer.render(commandCall, result) override fun process(model: Model, eval: Evaluator, recorder: ResultRecorder) = model.apply { dbTester.seed(model.seed, eval) - } + }.copy(seed = dbTester.metaData(model.seed)) data class Model(val seed: TableSeed, val caption: String?) diff --git a/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/commands/check/BaseResultRenderer.kt b/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/commands/check/BaseResultRenderer.kt index 5d020a0fa..b455a26fd 100644 --- a/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/commands/check/BaseResultRenderer.kt +++ b/exam-db/src/main/java/io/github/adven27/concordion/extensions/exam/db/commands/check/BaseResultRenderer.kt @@ -1,5 +1,6 @@ package io.github.adven27.concordion.extensions.exam.db.commands.check +import io.github.adven27.concordion.extensions.exam.core.Content import io.github.adven27.concordion.extensions.exam.core.html.Html import io.github.adven27.concordion.extensions.exam.core.html.div import io.github.adven27.concordion.extensions.exam.core.html.errorMessage @@ -97,10 +98,14 @@ open class BaseResultRenderer(private val printer: ValuePrinter) : DbCheckComman ?: td.success(expected.tableName(), col, expected[row, col], actual[row, col]) } - private fun Html.diff(d: Difference) = this( - Html("del", printer.print(d.expectedTable.tableName(), d.columnName, d.expectedValue), "class" to "me-1"), - Html("ins", printer.print(d.actualTable.tableName(), d.columnName, d.actualValue)) - ).css("table-danger").tooltip(d.failMessage) + private fun Html.diff(d: Difference): Html { + val act = d.actualValue + val exp = if (act is Content) Content(body = d.expectedValue.toString(), type = act.type) else d.expectedValue + return this( + Html("del", printer.print(d.actualTable.tableName(), d.columnName, exp)).css("expected"), + Html("ins", printer.print(d.actualTable.tableName(), d.columnName, act)).css("actual") + ).css("${if (act is Content) act.type else ""} failure").tooltip(d.failMessage) + } private operator fun List.get(row: Int, col: String) = singleOrNull { it.rowIndex == row && it.columnName.equals(col, ignoreCase = true) } diff --git a/exam-mq-kafka/src/main/java/io/github/adven27/concordion/extensions/exam/mq/kafka/KafkaTester.kt b/exam-mq-kafka/src/main/java/io/github/adven27/concordion/extensions/exam/mq/kafka/KafkaTester.kt index 8f8f2e46f..a5d734a14 100644 --- a/exam-mq-kafka/src/main/java/io/github/adven27/concordion/extensions/exam/mq/kafka/KafkaTester.kt +++ b/exam-mq-kafka/src/main/java/io/github/adven27/concordion/extensions/exam/mq/kafka/KafkaTester.kt @@ -71,9 +71,6 @@ open class KafkaConsumeAndSendTester @JvmOverloads constructor( private fun toRecordHeader(it: Map.Entry) = RecordHeader(it.key, it.value?.toByteArray()) companion object : KLogging() { - private const val PARAM_PARTITION = "partition" - private const val PARAM_KEY = "key" - @JvmField val DEFAULT_PRODUCER_CONFIG: Map = mapOf( ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java.name, @@ -174,6 +171,10 @@ open class KafkaConsumeOnlyTester @JvmOverloads constructor( } companion object : KLogging() { + const val PARAM_PARTITION = "partition" + const val PARAM_KEY = "key" + const val PARAM_OFFSET = "offset" + const val PARAM_TIMESTAMP = "timestamp" const val POLL_MILLIS: Long = 50 private const val KAFKA_FETCHING_TIMEOUT: Long = 10 @@ -187,7 +188,16 @@ open class KafkaConsumeOnlyTester @JvmOverloads constructor( @JvmField val DEFAULT_RECORD_MAPPER: (ConsumerRecord) -> Message = { record -> - Message(record.value(), record.headers().associate { it.key() to String(it.value()) }) + Message( + body = record.value(), + headers = record.headers().associate { it.key() to String(it.value()) }, + params = mapOf( + PARAM_KEY to record.key(), + PARAM_PARTITION to record.partition().toString(), + PARAM_OFFSET to record.offset().toString(), + PARAM_TIMESTAMP to record.timestamp().toString() + ) + ) } } } diff --git a/example/build.gradle b/example/build.gradle index a615c3aa9..f236b2a00 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,25 +1,26 @@ +plugins { + id 'org.springframework.boot' version '3.2.2' + id 'io.spring.dependency-management' version '1.1.4' + id 'org.jetbrains.kotlin.plugin.spring' version "$kotlin_version" +} + dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.liquibase:liquibase-core' + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + implementation 'org.jetbrains.kotlin:kotlin-reflect' + runtimeOnly 'org.postgresql:postgresql' + implementation "ch.qos.logback:logback-classic:1.4.5" - implementation "io.ktor:ktor-server-core:$ktor_version" - implementation "io.ktor:ktor-server-netty:$ktor_version" - implementation "io.ktor:ktor-server-content-negotiation:$ktor_version" - implementation "io.ktor:ktor-serialization-jackson:$ktor_version" - implementation "org.jetbrains.exposed:exposed-core:$exposed_version" - implementation "org.jetbrains.exposed:exposed-dao:$exposed_version" - implementation "org.jetbrains.exposed:exposed-jdbc:$exposed_version" - implementation "org.jetbrains.exposed:exposed-java-time:$exposed_version" - implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonKt_version" - implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonKt_version" testImplementation project(':exam-ms') - + testImplementation "io.github.adven27:env-db-postgresql:5.1.1" testImplementation 'org.concordion:concordion-timing-extension:1.1.0' testImplementation 'org.concordion:concordion-run-totals-extension:1.2.0' - - testImplementation 'com.h2database:h2:2.1.214' } -test { +tasks.named('test') { include '**/Specs.class' systemProperty 'concordion.output.dir', "$reporting.baseDir/specs" systemProperty "SPECS_ADOC_VERSION", libVersion diff --git a/example/src/main/kotlin/app/DatabaseFactory.kt b/example/src/main/kotlin/app/DatabaseFactory.kt deleted file mode 100644 index 7c67ca9c9..000000000 --- a/example/src/main/kotlin/app/DatabaseFactory.kt +++ /dev/null @@ -1,57 +0,0 @@ -package app - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.SchemaUtils.create -import org.jetbrains.exposed.sql.Table -import org.jetbrains.exposed.sql.insert -import org.jetbrains.exposed.sql.javatime.datetime -import org.jetbrains.exposed.sql.transactions.transaction -import java.time.LocalDateTime - -@Suppress("MagicNumber") -object DatabaseFactory { - fun init() { - Database.connect( - "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS SA\\;SET SCHEMA SA", - driver = "org.h2.Driver", - user = "sa" - ) - transaction { - create(JobResult) - create(Widgets) - Widgets.insert { - it[name] = "widget one" - it[quantity] = 27 - it[updatedAt] = LocalDateTime.now() - } - Widgets.insert { - it[name] = "widget two" - it[quantity] = 14 - it[updatedAt] = LocalDateTime.now() - } - } - } - - suspend fun dbQuery(block: () -> T): T = withContext(Dispatchers.IO) { transaction { block() } } -} - -@Suppress("MagicNumber") -object Widgets : Table() { - val id = integer("id").autoIncrement() - val name = varchar("name", 10) - val quantity = integer("quantity") - val updatedAt = datetime("updated") - override val primaryKey = PrimaryKey(id) -} - -@Suppress("MagicNumber") -object JobResult : Table() { - val id = integer("id").autoIncrement() - val result = varchar("result", 10).nullable() - override val primaryKey = PrimaryKey(id) -} - -data class Widget(val id: Int, val name: String, val quantity: Int, val updatedAt: LocalDateTime) -data class NewWidget(val id: Int?, val name: String?, val quantity: Int?) diff --git a/example/src/main/kotlin/app/Main.kt b/example/src/main/kotlin/app/Main.kt deleted file mode 100644 index bb23b64d7..000000000 --- a/example/src/main/kotlin/app/Main.kt +++ /dev/null @@ -1,249 +0,0 @@ -@file:JvmName("SutApp") -@file:Suppress("MagicNumber") - -package app - -import com.fasterxml.jackson.databind.SerializationFeature -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer -import io.ktor.http.HttpStatusCode -import io.ktor.http.HttpStatusCode.Companion.BadRequest -import io.ktor.http.HttpStatusCode.Companion.Created -import io.ktor.serialization.jackson.jackson -import io.ktor.server.application.ApplicationCall -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.http.content.resources -import io.ktor.server.http.content.static -import io.ktor.server.netty.Netty -import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.request.header -import io.ktor.server.request.httpMethod -import io.ktor.server.request.receive -import io.ktor.server.request.receiveNullable -import io.ktor.server.request.receiveText -import io.ktor.server.request.uri -import io.ktor.server.response.respond -import io.ktor.server.response.respondText -import io.ktor.server.routing.delete -import io.ktor.server.routing.get -import io.ktor.server.routing.post -import io.ktor.server.routing.put -import io.ktor.server.routing.routing -import io.ktor.util.pipeline.PipelineContext -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq -import org.jetbrains.exposed.sql.deleteWhere -import org.jetbrains.exposed.sql.insert -import org.jetbrains.exposed.sql.select -import org.jetbrains.exposed.sql.selectAll -import org.jetbrains.exposed.sql.update -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter.ofPattern -import java.util.TimeZone - -private val engine = embeddedServer(Netty, port = System.getProperty("server.port").toInt(), host = "") { - install(ContentNegotiation) { - jackson { - setTimeZone(TimeZone.getDefault()) - registerModule( - JavaTimeModule() - .addSerializer( - LocalDateTime::class.java, - LocalDateTimeSerializer(ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")) - ) - ) - configure(SerializationFeature.INDENT_OUTPUT, true) - configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) - } - } - DatabaseFactory.init() - val jobs: MutableList = ArrayList() - val widgetService = WidgetService() - val jobService = JobService() - routing { - post("/jobs") { - call.application.environment.log.info( - "trigger job " + runCatching { call.receiveNullable() }.getOrNull() - ) - val id = jobService.add() - jobs.add(id) - - // do some hard work async - GlobalScope.launch { - delay(2000L) - jobService.update(id, "done") - jobs.remove(id) - } - call.respond("""{ "id" : $id }""") - } - - get("/jobs/{id}") { - call.application.environment.log.info("is job running?") - call.respond("""{ "running" : "${jobs.contains(call.parameters["id"]?.toInt()!!)}" }""") - } - - get("/widgets") { - call.application.environment.log.info("service.getAll: " + widgetService.getAll().toString()) - call.respond(widgetService.getAll()) - } - - get("/widgets/{id}") { - val widget = widgetService.getBy(call.parameters["id"]?.toInt()!!) - if (widget == null) call.respond(HttpStatusCode.NotFound) else call.respond(widget) - } - - post("/widgets") { - val widget = call.receive() - return@post when { - widget.name == null -> call.respond(BadRequest, mapOf("error" to "name is required")) - widget.quantity == null -> call.respond(BadRequest, mapOf("error" to "quantity is required")) - else -> { - try { - call.respond(Created, widgetService.add(widget)) - } catch (expected: Exception) { - call.respond(BadRequest, mapOf("error" to expected.message)) - } - } - } - } - - put("/widgets") { - val widget = call.receive() - val updated = widgetService.update(widget) - call.respond(HttpStatusCode.OK, updated) - } - - delete("/widgets/{id}") { - val removed = widgetService.delete(call.parameters["id"]?.toInt()!!) - if (removed) call.respond(HttpStatusCode.OK) else call.respond(HttpStatusCode.NotFound) - } - - post("/mirror/soap") { - call.respondText(call.receiveText(), io.ktor.http.ContentType.parse("application/soap+xml; charset=utf-8")) - } - put("/mirror/body") { call.respond(call.receiveText()) } - post("/mirror/request") { mirrorRequestWithBody() } - put("/mirror/request") { mirrorRequestWithBody() } - delete("/mirror/request") { mirrorRequest() } - get("/mirror/request") { mirrorRequest() } - get("/mirror/headers") { mirrorHeaders() } - delete("/mirror/headers") { mirrorHeaders() } - - get("/ignoreJson") { - call.respond( - """ - { - "param1":"value1", - "param2":"value2", - "arr": [{"param3":"value3", "param4":"value4"}, {"param3":"value3", "param4":"value4"}] - } - """.trimIndent() - ) - } - static("/ui") { - resources("files") - } - } -} - -private suspend fun PipelineContext.mirrorRequestWithBody() { - with(call) { - respond( - mapOf( - request.httpMethod.value to request.uri, - "body" to receive(Map::class) - ) - ) - } -} - -private suspend fun PipelineContext.mirrorRequest() { - call.respond(mapOf(call.request.httpMethod.value to call.request.uri)) -} - -private suspend fun PipelineContext.mirrorHeaders() { - with(call) { - respond( - mapOf( - request.httpMethod.value to request.uri, - "Authorization" to request.header("Authorization"), - "Accept-Language" to request.header("Accept-Language"), - "cookies" to request.cookies.rawCookies - ) - ) - } -} - -fun main() { - start() -} - -fun start() = engine.start() -fun stop() = engine.stop(1000L, 2000L) - -class WidgetService { - suspend fun getAll(): List = DatabaseFactory.dbQuery { Widgets.selectAll().map { toWidget(it) } } - - suspend fun getBy(id: Int): Widget? = DatabaseFactory.dbQuery { - Widgets.select { (Widgets.id eq id) }.mapNotNull { toWidget(it) }.singleOrNull() - } - - suspend fun update(widget: NewWidget) = widget.id?.let { - DatabaseFactory.dbQuery { - Widgets.update({ Widgets.id eq it }) { - it[name] = widget.name!! - it[quantity] = widget.quantity!! - it[updatedAt] = LocalDateTime.now() - } - } - getBy(it) - } ?: add(widget) - - suspend fun add(widget: NewWidget): Widget { - var key = 0 - DatabaseFactory.dbQuery { - key = ( - Widgets.insert { - it[name] = if (widget.name!!.isBlank()) throw BlankNotAllowed() else widget.name - it[quantity] = widget.quantity!! - it[updatedAt] = LocalDateTime.now() - } get Widgets.id - ) - } - return getBy(key)!! - } - - class BlankNotAllowed : RuntimeException("blank value not allowed") - - suspend fun delete(id: Int): Boolean = DatabaseFactory.dbQuery { Widgets.deleteWhere { Widgets.id eq id } > 0 } - - private fun toWidget(row: ResultRow) = Widget( - id = row[Widgets.id], - name = row[Widgets.name], - quantity = row[Widgets.quantity], - updatedAt = row[Widgets.updatedAt] - ) -} - -class JobService { - suspend fun add(): Int { - var key = 0 - DatabaseFactory.dbQuery { - key = (JobResult.insert {} get JobResult.id) - } - return key - } - - suspend fun update(id: Int, jobResult: String) { - DatabaseFactory.dbQuery { - JobResult.update({ JobResult.id eq id }) { - it[result] = jobResult - } - } - } -} diff --git a/example/src/main/kotlin/com/example/demo/DemoApplication.kt b/example/src/main/kotlin/com/example/demo/DemoApplication.kt new file mode 100644 index 000000000..fa8aa24b3 --- /dev/null +++ b/example/src/main/kotlin/com/example/demo/DemoApplication.kt @@ -0,0 +1,40 @@ +package com.example.demo + +import jakarta.servlet.http.HttpServletRequest +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus.OK +import org.springframework.http.MediaType.parseMediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@SpringBootApplication +@RestController +class DemoApplication { + + @PostMapping("/mirror/soap") + fun soap(@RequestBody body: String) = ResponseEntity(body, soapHeader(), OK) + + private fun soapHeader() = + HttpHeaders().apply { contentType = parseMediaType("application/soap+xml; charset=utf-8") } + + @RequestMapping("/mirror/request") + fun mirror(@RequestBody body: Map?, r: HttpServletRequest) = listOfNotNull( + r.method to r.uri(), + body?.let { "body" to it }, + r.headerNames.toList().takeIf { it.isNotEmpty() }?.let { n -> "headers" to n.associateWith(r::getHeader) }, + r.cookies?.takeIf { it.isNotEmpty() }?.let { c -> "cookies" to c.associate { it.name to it.value } } + ).toMap() + + private fun HttpServletRequest.uri() = + requestURI + (queryString.takeUnless { it.isNullOrBlank() }?.let { "?$it" } ?: "") +} + +@Suppress("SpreadOperator") +fun main(args: Array) { + runApplication(*args) +} diff --git a/example/src/main/resources/application.conf b/example/src/main/resources/application.conf deleted file mode 100644 index 52071aae7..000000000 --- a/example/src/main/resources/application.conf +++ /dev/null @@ -1,9 +0,0 @@ -ktor { - deployment { - port = 8080 - } - - application { - modules = [ Main.app.main ] - } -} \ No newline at end of file diff --git a/example/src/main/resources/application.yaml b/example/src/main/resources/application.yaml new file mode 100644 index 000000000..6cc164320 --- /dev/null +++ b/example/src/main/resources/application.yaml @@ -0,0 +1,6 @@ +spring: + datasource: + driver-class-name: ${env.db.postgresql.driver} + url: ${env.db.postgresql.url} + username: ${env.db.postgresql.username} + password: ${env.db.postgresql.password} diff --git a/example/src/main/resources/db/changelog/changes/db.changelog-1.0.sql b/example/src/main/resources/db/changelog/changes/db.changelog-1.0.sql new file mode 100644 index 000000000..3a7e3d406 --- /dev/null +++ b/example/src/main/resources/db/changelog/changes/db.changelog-1.0.sql @@ -0,0 +1,34 @@ +--liquibase formatted sql + +--changeset author:init +create table person +( + id int primary key generated by default as identity, + name varchar, + age int, + birthday timestamp +); +create table person_fields +( + id int primary key generated by default as identity, + name varchar, + val varchar, + person_id integer references person +); +create table product +( + id int primary key generated by default as identity, + name varchar, + price numeric(20, 2), + rating integer, + disabled boolean, + created_at timestamp, + meta_json jsonb +); +create table content_types +( + id int primary key generated by default as identity, + data_xml varchar, + data_json jsonb, + data_json_with_extra_fields jsonb +); diff --git a/example/src/main/resources/db/changelog/db.changelog-master.yaml b/example/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 000000000..d58bd6f56 --- /dev/null +++ b/example/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,3 @@ +databaseChangeLog: + - include: + file: db/changelog/changes/db.changelog-1.0.sql diff --git a/example/src/main/resources/files/dummy.html b/example/src/main/resources/files/dummy.html deleted file mode 100644 index c799d5ccb..000000000 --- a/example/src/main/resources/files/dummy.html +++ /dev/null @@ -1,7 +0,0 @@ - - - -

Web UI Example

-

Hello world!

- - \ No newline at end of file diff --git a/example/src/test/kotlin/specs/Specs.kt b/example/src/test/kotlin/specs/Specs.kt index ebc37124d..2222498ef 100644 --- a/example/src/test/kotlin/specs/Specs.kt +++ b/example/src/test/kotlin/specs/Specs.kt @@ -1,10 +1,10 @@ package specs -import app.start -import app.stop +import com.example.demo.DemoApplication import com.github.jknack.handlebars.Helper import io.github.adven27.concordion.extensions.exam.core.AbstractSpecs import io.github.adven27.concordion.extensions.exam.core.ExamExtension +import io.github.adven27.concordion.extensions.exam.core.ExamExtension.Companion.VERIFIER_XML import io.github.adven27.concordion.extensions.exam.core.JsonVerifier import io.github.adven27.concordion.extensions.exam.core.handlebars.date.DateHelpers import io.github.adven27.concordion.extensions.exam.core.handlebars.matchers.MatcherHelpers @@ -15,31 +15,38 @@ import io.github.adven27.concordion.extensions.exam.db.DbPlugin.ValuePrinter.Def import io.github.adven27.concordion.extensions.exam.db.DbTester import io.github.adven27.concordion.extensions.exam.db.DbUnitConfig import io.github.adven27.concordion.extensions.exam.db.DbUnitConfig.TableColumnValueComparer -import io.github.adven27.concordion.extensions.exam.db.commands.IgnoreMillisComparer -import io.github.adven27.concordion.extensions.exam.db.commands.JsonColumnComparer +import io.github.adven27.concordion.extensions.exam.db.commands.VerifierColumnComparer import io.github.adven27.concordion.extensions.exam.mq.MqPlugin import io.github.adven27.concordion.extensions.exam.mq.MqTester import io.github.adven27.concordion.extensions.exam.mq.MqTester.Message import io.github.adven27.concordion.extensions.exam.ws.WsPlugin +import io.github.adven27.env.core.Environment +import io.github.adven27.env.db.postgresql.PostgreSqlContainerSystem import net.javacrumbs.jsonunit.core.Option.IGNORING_EXTRA_FIELDS -import java.util.ArrayDeque +import org.springframework.boot.SpringApplication +import org.springframework.context.ConfigurableApplicationContext +import java.util.* class Nested : Specs() class WsCheckFailures : Specs() class MqCheckFailures : Specs() class DbSetOperations : Specs() class DbCheckFailures : Specs() +class DbCheckContentTypes : Specs() @Suppress("FunctionOnlyReturningConstant") open class Specs : AbstractSpecs() { override fun init(): ExamExtension = ExamExtension( - WsPlugin(PORT.also { System.setProperty("server.port", it.toString()) }), + WsPlugin(port = ENV.sutPort()), MqPlugin("myQueue" to DummyMq(), "myAnotherQueue" to DummyMq()), DbPlugin( dbTester, valuePrinter = ValuePrinter.Default( - tableColumnType = mapOf(TableColumn("product", "meta_json") to "json") + tableColumnStyle = mapOf( + TableColumn("content_types", "data_xml") to "xml", + TableColumn("content_types", "data_json_with_extra_fields") to "details" + ) ) ) ).withHandlebar { hb -> @@ -58,11 +65,11 @@ open class Specs : AbstractSpecs() { ) override fun startSut() { - start() + SUT = SpringApplication(DemoApplication::class.java).run() } override fun stopSut() { - stop() + SUT.stop() } private val users = mutableListOf() @@ -85,27 +92,29 @@ open class Specs : AbstractSpecs() { val miscHelpers: String = MiscHelpers.entries.joinToString("\n") { it.describe() } companion object { - const val PORT = 8888 + private lateinit var SUT: ConfigurableApplicationContext + val ENV: ApplicationEnvironment = ApplicationEnvironment().apply { up() } @JvmStatic - val dbTester = DbTester( - driver = "org.h2.Driver", - url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM 'classpath:sql/populate.sql'", - user = "sa", - password = "", - dbUnitConfig = DbUnitConfig( - tableColumnValueComparer = listOf( - TableColumnValueComparer( - table = "types", - columnValueComparer = mapOf("DATETIME_TYPE" to IgnoreMillisComparer()) - ), - TableColumnValueComparer( - table = "product", - columnValueComparer = mapOf("META_JSON" to JsonColumnComparer()) + val dbTester = with(ENV.database()) { + DbTester( + driver = driver, + url = jdbcUrl, + user = username, + password = password, + dbUnitConfig = DbUnitConfig( + tableColumnValueComparer = listOf( + TableColumnValueComparer( + table = "content_types", + columnValueComparer = mapOf( + "data_json_with_extra_fields" to VerifierColumnComparer("jsonIgnoreExtraFields"), + "data_xml" to VerifierColumnComparer(VERIFIER_XML) + ) + ) ) ) ) - ) + } private class DummyMq : MqTester.NOOP() { private val queue = ArrayDeque() @@ -117,3 +126,10 @@ open class Specs : AbstractSpecs() { } } } + +class ApplicationEnvironment : Environment( + "DB" to PostgreSqlContainerSystem() +) { + fun database() = env().config + fun sutPort() = 8888.also { System.setProperty("server.port", it.toString()) } +} diff --git a/example/src/test/resources/data/db/content_types.csv b/example/src/test/resources/data/db/content_types.csv new file mode 100644 index 000000000..86b2fbb49 --- /dev/null +++ b/example/src/test/resources/data/db/content_types.csv @@ -0,0 +1,21 @@ +data_json, data_json_with_extra_fields, data_xml +"{ + ""id"": ""a"", + ""code"": 1 +}","{ + ""id"": ""a"", + ""code"": 1, + ""extra"": ""ignore"" +}"," + {{iso (at)}} +" +"{ + ""id"": ""b"", + ""code"": 2 +}","{ + ""id"": ""b"", + ""code"": 2, + ""extra"": ""ignore"" +}"," + any +" diff --git a/example/src/test/resources/data/ws/delete-resp.http b/example/src/test/resources/data/ws/delete-resp.http index 737563441..908b0c152 100644 --- a/example/src/test/resources/data/ws/delete-resp.http +++ b/example/src/test/resources/data/ws/delete-resp.http @@ -1,4 +1,7 @@ -200 OK +200 Content-Type: application/json -{"DELETE":"/mirror/request"} +{ + "DELETE":"/mirror/request", + "headers": { "host": "{{ignore}}", "content-type": "application/json" } +} diff --git a/example/src/test/resources/data/ws/get-resp.http b/example/src/test/resources/data/ws/get-resp.http index b6e0246fd..c37be05b0 100644 --- a/example/src/test/resources/data/ws/get-resp.http +++ b/example/src/test/resources/data/ws/get-resp.http @@ -1,4 +1,7 @@ -200 OK +200 Content-Type: application/json -{"GET":"/mirror/request"} +{ + "GET":"/mirror/request", + "headers": { "host": "{{ignore}}", "content-type": "application/json" } +} diff --git a/example/src/test/resources/data/ws/post-resp.http b/example/src/test/resources/data/ws/post-resp.http index 53cb92713..7184cb7d4 100644 --- a/example/src/test/resources/data/ws/post-resp.http +++ b/example/src/test/resources/data/ws/post-resp.http @@ -1,4 +1,8 @@ -200 OK +200 Content-Type: application/json -{"POST":"/mirror/request?p=1","body":{"a": "ok", "b": 1 }} +{ + "POST":"/mirror/request?p=1", + "headers": { "host": "{{ignore}}", "content-type": "application/json", "content-length": "{{ignore}}" }, + "body":{"a": "ok", "b": 1 } +} diff --git a/example/src/test/resources/data/ws/put-resp.http b/example/src/test/resources/data/ws/put-resp.http index cb49c976b..a1acebbad 100644 --- a/example/src/test/resources/data/ws/put-resp.http +++ b/example/src/test/resources/data/ws/put-resp.http @@ -1,4 +1,8 @@ -200 OK +200 Content-Type: application/json -{"PUT":"/mirror/request?p=1","body":{"a": "ok", "b": 1 }} +{ + "PUT":"/mirror/request?p=1", + "headers": { "host": "{{ignore}}", "content-type": "application/json", "content-length": "{{ignore}}" }, + "body":{"a": "ok", "b": 1 } +} diff --git a/example/src/test/resources/specs/DbCheckContentTypes.adoc b/example/src/test/resources/specs/DbCheckContentTypes.adoc new file mode 100644 index 000000000..b27a7eac6 --- /dev/null +++ b/example/src/test/resources/specs/DbCheckContentTypes.adoc @@ -0,0 +1,80 @@ += DB Check Content Types + +[#before] +.Before each example +**** +.Given table `content_types` +[%header,format=csv, e-db-set=content_types] +|=== +include::../data/db/content_types.csv[] +|=== +**** + +.Check content types +[source,asciidoc] +-- +include::DbCheckContentTypes.adoc[tags=db-check-ct] +-- + +.Check content types +==== +//tag::db-check-ct[] +[%header, cols="1a,1a,1a", e-db-check=content_types, e-orderBy="id"] +|=== +|data_json |data_json_with_extra_fields |data_xml + +|[source,json] +---- +{ "id": "a", "code": 1 } +---- +|[source,json] +---- +{ "id": "a", "code": 1 } +---- +|[source,xml] +---- + {{iso (at)}} +---- + +|[source,json] +---- +{ "id": "{{string}}", "code": 2 } +---- +|[source,json] +---- +{ "id": "b", "code": "{{number}}" } +---- +|[source,xml] +---- + {{ignore}} +---- +|=== +//end::db-check-ct[] +==== + +[.ExpectedToFail] +.Failures +==== +[%header, cols="1a,1a", e-db-check=content_types, e-orderBy="id"] +|=== +|data_json |data_xml + +|[source,json] +---- +{ "id": "{{number}}", "code": 1 } +---- +|[source,xml] +---- + {{iso (at)}} +---- + +|[source,json] +---- +{ "id": "{{string}}", "code": 2 } +---- +|[source,xml] +---- + {{number}} +---- +|=== +==== diff --git a/example/src/test/resources/specs/plugin-db.adoc b/example/src/test/resources/specs/plugin-db.adoc index 43690499c..5077d5415 100644 --- a/example/src/test/resources/specs/plugin-db.adoc +++ b/example/src/test/resources/specs/plugin-db.adoc @@ -145,15 +145,12 @@ include::plugin-db.adoc[tags=db-check-ordered] ==== //tag::db-check-ordered[] [e-db-check=product, e-orderBy='rating, id'] -|=== -|name |rating - -|{{string}} -|{{ignore}} +,=== +name, rating -|{{string}} -|10 -|=== +{{string}}, {{ignore}} +{{string}}, 10 +,=== //end::db-check-ordered[] ==== @@ -322,3 +319,4 @@ id, name - link:DbSetOperations.adoc[What are set operation options?, role=e-run] - link:DbCheckFailures.adoc[How failures look like?, role=e-run] +- link:DbCheckContentTypes.adoc[How to check content types?, role=e-run] diff --git a/example/src/test/resources/specs/plugin-ws.adoc b/example/src/test/resources/specs/plugin-ws.adoc index e921db4ae..5bf0a83f3 100644 --- a/example/src/test/resources/specs/plugin-ws.adoc +++ b/example/src/test/resources/specs/plugin-ws.adoc @@ -49,7 +49,7 @@ include::plugin-ws.adoc[tags=ws-http] .Given request: [source,httprequest] ---- -GET /mirror/headers +GET /mirror/request Content-Type: application/json Authorization: token Accept-Language: ru @@ -59,13 +59,18 @@ Cookie: cookie1=c1; cookie2=c2 .Expected response: [source,httprequest] ---- -200 OK +200 Content-Type: application/json { - "GET": "/mirror/headers", - "Authorization": "token", - "Accept-Language": "ru", + "GET": "/mirror/request", + "headers": { + "host": "{{ignore}}", + "content-type": "application/json", + "authorization": "token", + "accept-language": "ru", + "cookie": "cookie1=c1; cookie2=c2" + }, "cookies": { "cookie1": "c1", "cookie2": "c2" } } ---- @@ -161,12 +166,11 @@ Content-Type: application/json ---- .Check interactions -[%header,cols="4,4,1,1",e-verify-rows="#i : #interactions"] +[%header,cols="4,2,0",e-verify-rows="#i : #interactions"] |=== |[e-eq=#i.req.raw]#Request# |[e-verifier=jsonIgnoreExtraFields e-eq=#i.resp.body]#Response# |[e-eq=#i.resp.statusCode]#Status# -|[e-eq=#i.resp.statusPhrase]#Phrase# |[pre language-http]`GET /mirror/request?p=1 HTTP/1.1 Host: localhost:8888 @@ -175,7 +179,6 @@ Content-Type: application/json {blank}` |[pre language-json]`{}` |200 -|OK |=== //end::ws-http-v[] ==== @@ -207,10 +210,18 @@ Content-Type: application/json .Check with registered `jsonIgnoreExtraFields` verifier [source,httprequest,e-verifier=jsonIgnoreExtraFields] ---- -200 OK +200 Content-Type: application/json -{ "POST": "/mirror/request" } +{ + "POST": "/mirror/request", + "body": {}, + "headers": { + "host": "{{ignore}}", + "content-type": "application/json", + "content-length": "{{ignore}}" + } +} ---- -- //end::ws-custom[] diff --git a/example/src/test/resources/sql/populate.sql b/example/src/test/resources/sql/populate.sql deleted file mode 100644 index 1b9791a87..000000000 --- a/example/src/test/resources/sql/populate.sql +++ /dev/null @@ -1,16 +0,0 @@ -create schema if not exists sa; -set schema sa; -create table if not exists person (id integer primary key, name varchar, age number, iq number, birthday timestamp); -create table if not exists person_fields (id integer primary key, name varchar, val varchar, person_id integer, foreign key (person_id) references person(id)); -create table if not exists empty (name varchar, val number); -create table if not exists types (id integer primary key, timestamp_type timestamp, datetime_type smalldatetime, date_type date); -create table if not exists product (id integer primary key, name varchar, price numeric(20, 2), rating integer, disabled boolean, created_at timestamp, meta_json varchar); - -create table if not exists orders ( - id INTEGER primary key, - status VARCHAR, - client number, - driver number, - created timestamp, - updated timestamp -); diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e411586a5..1af9e0930 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists