Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b023ad364 | ||
|
|
7a8ded192c | ||
| 370d8fb81d | |||
| ca85a7ac64 | |||
| 2328e7ebe2 | |||
|
|
4bc14b5340 | ||
|
|
e7b67eea89 | ||
| 3ffabf3011 | |||
| 7f2609d0a5 | |||
| 1fb647b733 | |||
| bea0d78d75 |
@@ -19,12 +19,14 @@ end_of_line = crlf
|
|||||||
|
|
||||||
# noinspection EditorConfigKeyCorrectness
|
# noinspection EditorConfigKeyCorrectness
|
||||||
[*.{kt,kts}]
|
[*.{kt,kts}]
|
||||||
ij_kotlin_packages_to_use_import_on_demand = unset
|
|
||||||
ij_kotlin_allow_trailing_comma_on_call_site = true
|
|
||||||
ij_kotlin_allow_trailing_comma = true
|
ij_kotlin_allow_trailing_comma = true
|
||||||
ktlint_standard_import-ordering = disabled
|
ij_kotlin_allow_trailing_comma_on_call_site = true
|
||||||
ktlint_standard_no-empty-first-line-in-class-body = disabled
|
ij_kotlin_packages_to_use_import_on_demand = unset
|
||||||
|
ktlint_standard_chain-method-continuation = disabled
|
||||||
|
ktlint_standard_class-signature = disabled
|
||||||
ktlint_standard_function-signature = disabled
|
ktlint_standard_function-signature = disabled
|
||||||
ktlint_standard_parameter-list-wrapping = disabled
|
ktlint_standard_import-ordering = disabled
|
||||||
ktlint_standard_multiline-expression-wrapping = disabled
|
ktlint_standard_multiline-expression-wrapping = disabled
|
||||||
|
ktlint_standard_no-empty-first-line-in-class-body = disabled
|
||||||
|
ktlint_standard_parameter-list-wrapping = disabled
|
||||||
ktlint_standard_string-template-indent = disabled
|
ktlint_standard_string-template-indent = disabled
|
||||||
|
|||||||
12
.github/workflows/release.yml
vendored
Normal file
12
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
uses: swordsteel/hlaeja-common-workflows/.github/workflows/release.yml@master
|
||||||
|
secrets:
|
||||||
|
CI_BOT_PAT: ${{ secrets.CI_BOT_PAT }}
|
||||||
|
with:
|
||||||
|
TYPE: artifact
|
||||||
12
.github/workflows/run-checks.yml
vendored
Normal file
12
.github/workflows/run-checks.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
name: Pull request validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- '.github/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
uses: swordsteel/hlaeja-common-workflows/.github/workflows/run-checks.yml@master
|
||||||
|
secrets:
|
||||||
|
CI_BOT_PAT: ${{ secrets.CI_BOT_PAT }}
|
||||||
29
README.md
29
README.md
@@ -2,26 +2,35 @@
|
|||||||
|
|
||||||
In the forge of software development, where annotations ignite, A crucible of testing, common classes to excite. Each annotation examined, with attention to detail and might, Their effects on code behavior, tested through day and night. From mockk objects to test doubles, a toolkit to refine, Developers and testers, their skills to redefine. The Annotation Validator, a sentinel of code integrity true, A library of verification, where testing wisdom shines anew.
|
In the forge of software development, where annotations ignite, A crucible of testing, common classes to excite. Each annotation examined, with attention to detail and might, Their effects on code behavior, tested through day and night. From mockk objects to test doubles, a toolkit to refine, Developers and testers, their skills to redefine. The Annotation Validator, a sentinel of code integrity true, A library of verification, where testing wisdom shines anew.
|
||||||
|
|
||||||
|
## Kafka and Postgres Test Container
|
||||||
|
|
||||||
|
`@KafkaPostgresTestContainer` Annotation for integration tests.
|
||||||
|
|
||||||
|
This combined `@KafkaTestContainer` and `@PostgresTestContainer`
|
||||||
|
|
||||||
|
## Kafka Test Container
|
||||||
|
|
||||||
|
`@KafkaTestContainer` Annotation for integration tests.
|
||||||
|
|
||||||
|
Initialize Kafka test container using test container default properties
|
||||||
|
|
||||||
## Postgres Test Container
|
## Postgres Test Container
|
||||||
|
|
||||||
`@PostgresContainer` Annotation for integration tests.
|
`@PostgresTestContainer` Annotation for integration tests.
|
||||||
|
|
||||||
Initialize Postgres test container using spring properties for R2DBC,
|
Initialize Postgres test container using test container default properties, script located in `src/<test path>/resources/postgres/` folder.
|
||||||
script located in `src/<test path>/resources` folder. And specify properties in `src/<test path>/resources/application.properties`.
|
|
||||||
|
|
||||||
### Properties For Test Container
|
### Properties For Test Container
|
||||||
|
|
||||||
| name | required | example | info |
|
| file | required | info |
|
||||||
|----------------------------|:--------:|-------------|----------------------------------------------------------------|
|
|---------------------|:--------:|----------------------------------------------------------------|
|
||||||
| container.postgres.version | | postgres:17 | postgres container default postgres:latest |
|
| postgres/schema.sql | ✓ | Postgres init script containing all structure and functions |
|
||||||
| container.postgres.init | ✓ | schema.sql | Postgres init script containing all structure and functions |
|
| postgres/data.sql | | Postgres data script containing all data to populate database |
|
||||||
| container.postgres.before | | data.sql | Postgres data script containing all data to populate database |
|
| postgres/reset.sql | | Postgres reset script containing all command to reset database |
|
||||||
| container.postgres.after | | reset.sql | Postgres reset script containing all command to reset database |
|
|
||||||
|
|
||||||
## Releasing library
|
## Releasing library
|
||||||
|
|
||||||
Run `release.sh` script from `master` branch.
|
Run release pipeline from `master` branch.
|
||||||
|
|
||||||
## Publishing library
|
## Publishing library
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(hlaeja.plugins.kotlin.jvm)
|
alias(hlaeja.plugins.kotlin.jvm)
|
||||||
alias(hlaeja.plugins.kotlin.spring)
|
alias(hlaeja.plugins.kotlin.spring)
|
||||||
alias(hlaeja.plugins.ltd.hlaeja.plugin.library)
|
alias(hlaeja.plugins.spring.boot)
|
||||||
alias(hlaeja.plugins.spring.dependency.management)
|
alias(hlaeja.plugins.spring.dependency.management)
|
||||||
alias(hlaeja.plugins.springframework.boot)
|
alias(hlaeja.plugins.library)
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -12,6 +12,7 @@ dependencies {
|
|||||||
implementation(hlaeja.springboot.starter.r2dbc)
|
implementation(hlaeja.springboot.starter.r2dbc)
|
||||||
implementation(hlaeja.springboot.starter.test)
|
implementation(hlaeja.springboot.starter.test)
|
||||||
implementation(hlaeja.testcontainers.junit)
|
implementation(hlaeja.testcontainers.junit)
|
||||||
|
implementation(hlaeja.testcontainers.kafka)
|
||||||
implementation(hlaeja.testcontainers.postgresql)
|
implementation(hlaeja.testcontainers.postgresql)
|
||||||
|
|
||||||
testRuntimeOnly(hlaeja.junit.platform.launcher)
|
testRuntimeOnly(hlaeja.junit.platform.launcher)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
version=0.2.0
|
version=0.5.0-SNAPSHOT
|
||||||
catalog=0.10.0
|
catalog=0.12.0
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
9
gradlew
vendored
9
gradlew
vendored
@@ -86,8 +86,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
' "$PWD" ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -115,7 +114,7 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH="\\\"\\\""
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@@ -206,7 +205,7 @@ fi
|
|||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-classpath "$CLASSPATH" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
4
gradlew.bat
vendored
4
gradlew.bat
vendored
@@ -70,11 +70,11 @@ goto fail
|
|||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|||||||
89
release.sh
89
release.sh
@@ -1,89 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
### This should be a pipeline, but for this example let use this ###
|
|
||||||
|
|
||||||
check_active_branch() {
|
|
||||||
if [ "$(git rev-parse --abbrev-ref HEAD)" != "$1" ]; then
|
|
||||||
echo "Error: The current branch is not $1."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_uncommitted_changes() {
|
|
||||||
if [ -n "$(git status --porcelain)" ]; then
|
|
||||||
echo "Error: There are uncommitted changes in the repository."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
prepare_environment() {
|
|
||||||
git fetch origin
|
|
||||||
}
|
|
||||||
|
|
||||||
check_last_commit() {
|
|
||||||
last_commit_message=$(git log -1 --pretty=format:%s)
|
|
||||||
if [ "$last_commit_message" = "[RELEASE] - bump version" ]; then
|
|
||||||
echo "Warning: Nothing to release!!!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_differences() {
|
|
||||||
if ! git diff --quiet origin/"$1" "$1"; then
|
|
||||||
echo "Error: The branches origin/$1 and $1 have differences."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
un_snapshot_version() {
|
|
||||||
sed -i "s/\($1\s*=\s*[0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/" gradle.properties
|
|
||||||
}
|
|
||||||
|
|
||||||
current_version() {
|
|
||||||
awk -F '=' '/version\s*=\s*[0-9.]*/ {gsub(/^ +| +$/,"",$2); print $2}' gradle.properties
|
|
||||||
}
|
|
||||||
|
|
||||||
stage_files() {
|
|
||||||
for file in "$@"; do
|
|
||||||
if git diff --exit-code --quiet -- "$file"; then
|
|
||||||
echo "No changes in $file"
|
|
||||||
else
|
|
||||||
git add "$file"
|
|
||||||
echo "Changes in $file staged for commit"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
commit_change() {
|
|
||||||
stage_files gradle.properties
|
|
||||||
git commit -m "[RELEASE] - $1"
|
|
||||||
git push --porcelain origin master
|
|
||||||
}
|
|
||||||
|
|
||||||
add_release_tag() {
|
|
||||||
gitTag="v$(current_version)"
|
|
||||||
git tag -a "$gitTag" -m "Release version $gitTag"
|
|
||||||
git push --porcelain origin "$gitTag"
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshot_version() {
|
|
||||||
new_version="$(current_version | awk -F '.' '{print $1 "." $2+1 ".0"}')"
|
|
||||||
sed -i "s/\(version\s*=\s*\)[0-9.]*/\1$new_version-SNAPSHOT/" gradle.properties
|
|
||||||
}
|
|
||||||
|
|
||||||
# check and prepare for release
|
|
||||||
check_active_branch master
|
|
||||||
check_uncommitted_changes
|
|
||||||
prepare_environment
|
|
||||||
check_last_commit
|
|
||||||
check_differences master
|
|
||||||
|
|
||||||
# un-snapshot version for release
|
|
||||||
un_snapshot_version version
|
|
||||||
un_snapshot_version catalog
|
|
||||||
|
|
||||||
# release changes and prepare for next release
|
|
||||||
commit_change "release version: $(current_version)"
|
|
||||||
add_release_tag
|
|
||||||
snapshot_version
|
|
||||||
commit_change 'bump version'
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ltd.hlaeja.test.container
|
||||||
|
|
||||||
|
import ltd.hlaeja.test.container.extension.KafkaPostgresTestExtension
|
||||||
|
import ltd.hlaeja.test.container.postgres.PostgresTestListener
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.springframework.test.context.ContextConfiguration
|
||||||
|
import org.springframework.test.context.TestExecutionListeners
|
||||||
|
import org.springframework.test.context.TestExecutionListeners.MergeMode
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@ExtendWith(KafkaPostgresTestExtension::class)
|
||||||
|
@ContextConfiguration(initializers = [KafkaPostgresTestExtension::class])
|
||||||
|
@TestExecutionListeners(
|
||||||
|
listeners = [PostgresTestListener::class],
|
||||||
|
mergeMode = MergeMode.MERGE_WITH_DEFAULTS,
|
||||||
|
)
|
||||||
|
annotation class KafkaPostgresTestContainer
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ltd.hlaeja.test.container
|
||||||
|
|
||||||
|
import ltd.hlaeja.test.container.extension.KafkaTestExtension
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.springframework.test.context.ContextConfiguration
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@ExtendWith(KafkaTestExtension::class)
|
||||||
|
@ContextConfiguration(initializers = [KafkaTestExtension::class])
|
||||||
|
annotation class KafkaTestContainer
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package ltd.hlaeja.test.container
|
|
||||||
|
|
||||||
import org.springframework.test.context.ContextConfiguration
|
|
||||||
import org.springframework.test.context.TestExecutionListeners
|
|
||||||
import org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
@ContextConfiguration(initializers = [PostgresInitializer::class])
|
|
||||||
@TestExecutionListeners(listeners = [PostgresInitializer::class], mergeMode = MERGE_WITH_DEFAULTS)
|
|
||||||
annotation class PostgresContainer
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package ltd.hlaeja.test.container
|
|
||||||
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import ltd.hlaeja.test.util.isResourceFile
|
|
||||||
import org.springframework.r2dbc.core.DatabaseClient
|
|
||||||
import org.springframework.r2dbc.core.await
|
|
||||||
import org.springframework.stereotype.Component
|
|
||||||
|
|
||||||
private val log = KotlinLogging.logger {}
|
|
||||||
|
|
||||||
@Component
|
|
||||||
class PostgresExecutor(
|
|
||||||
private val databaseClient: DatabaseClient,
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun executeSqlFile(
|
|
||||||
sqlFile: String,
|
|
||||||
) = runBlocking {
|
|
||||||
sqlFile.isResourceFile()
|
|
||||||
?.inputStream
|
|
||||||
?.use {
|
|
||||||
log.debug { "Executing SQL file: $sqlFile" }
|
|
||||||
executeSqlStatements(makeSqlStatements(it))
|
|
||||||
}
|
|
||||||
?: log.debug { "SQL file not found or not readable: $sqlFile" }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("TooGenericExceptionThrown")
|
|
||||||
private suspend fun executeSqlStatements(
|
|
||||||
statements: List<String>,
|
|
||||||
) = try {
|
|
||||||
statements.forEach { statement ->
|
|
||||||
log.debug { "Running statement: $statement" }
|
|
||||||
databaseClient.sql(statement).await()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw RuntimeException("Failed to execute SQL statements", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun makeSqlStatements(
|
|
||||||
inputStream: InputStream,
|
|
||||||
): List<String> = BufferedReader(InputStreamReader(inputStream))
|
|
||||||
.lines()
|
|
||||||
.filter { it.isNotEmpty() && !it.startsWith("--") }
|
|
||||||
.map { it.trim() }
|
|
||||||
.toList()
|
|
||||||
.joinToString(" ")
|
|
||||||
.split(';')
|
|
||||||
.filter { it.isNotBlank() }
|
|
||||||
.map { "${it.trim()};" }
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package ltd.hlaeja.test.container
|
|
||||||
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
|
||||||
import ltd.hlaeja.test.util.getProperty
|
|
||||||
import ltd.hlaeja.test.util.isResourceFile
|
|
||||||
import org.springframework.boot.test.util.TestPropertyValues
|
|
||||||
import org.springframework.context.ApplicationContextInitializer
|
|
||||||
import org.springframework.context.ConfigurableApplicationContext
|
|
||||||
import org.springframework.test.context.TestContext
|
|
||||||
import org.springframework.test.context.TestExecutionListener
|
|
||||||
import org.testcontainers.containers.PostgreSQLContainer
|
|
||||||
|
|
||||||
private val log = KotlinLogging.logger {}
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
class PostgresInitializer : ApplicationContextInitializer<ConfigurableApplicationContext>, TestExecutionListener {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val SCRIPT_INIT = "container.postgres.init"
|
|
||||||
const val SCRIPT_BEFORE = "container.postgres.before"
|
|
||||||
const val SCRIPT_AFTER = "container.postgres.after"
|
|
||||||
const val POSTGRES_VERSION = "container.postgres.version"
|
|
||||||
const val POSTGRES_LATEST = "postgres:latest"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun initialize(
|
|
||||||
context: ConfigurableApplicationContext,
|
|
||||||
) {
|
|
||||||
postgres(context).apply {
|
|
||||||
TestPropertyValues.of(
|
|
||||||
"spring.r2dbc.url=r2dbc:pool:postgresql://$host:$firstMappedPort/$databaseName",
|
|
||||||
"spring.r2dbc.username=$username",
|
|
||||||
"spring.r2dbc.password=$password",
|
|
||||||
).applyTo(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun beforeTestClass(
|
|
||||||
context: TestContext,
|
|
||||||
) {
|
|
||||||
context.testClass
|
|
||||||
.also { log.debug { "Starting execution before class: ${it.simpleName}" } }
|
|
||||||
.getAnnotation(PostgresContainer::class.java) ?: return
|
|
||||||
context.getProperty(SCRIPT_BEFORE)
|
|
||||||
?.let { context.applicationContext.getBean(PostgresExecutor::class.java).executeSqlFile(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun afterTestClass(
|
|
||||||
context: TestContext,
|
|
||||||
) {
|
|
||||||
context.testClass
|
|
||||||
.also { log.debug { "Starting execution after class: ${it.simpleName}" } }
|
|
||||||
.getAnnotation(PostgresContainer::class.java) ?: return
|
|
||||||
context.getProperty(SCRIPT_AFTER)
|
|
||||||
?.let { context.applicationContext.getBean(PostgresExecutor::class.java).executeSqlFile(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun postgres(
|
|
||||||
context: ConfigurableApplicationContext,
|
|
||||||
): PostgreSQLContainer<*> = PostgreSQLContainer(context.getProperty(POSTGRES_VERSION, POSTGRES_LATEST)).apply {
|
|
||||||
context.getProperty(SCRIPT_INIT)
|
|
||||||
?.isResourceFile()
|
|
||||||
?.let { lala -> withInitScript(lala.path) }
|
|
||||||
?: log.error { "Postgres init script not found" }
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package ltd.hlaeja.test.container
|
||||||
|
|
||||||
|
import ltd.hlaeja.test.container.extension.PostgresTestExtension
|
||||||
|
import ltd.hlaeja.test.container.postgres.PostgresTestListener
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.springframework.test.context.ContextConfiguration
|
||||||
|
import org.springframework.test.context.TestExecutionListeners
|
||||||
|
import org.springframework.test.context.TestExecutionListeners.MergeMode
|
||||||
|
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@ExtendWith(PostgresTestExtension::class)
|
||||||
|
@ContextConfiguration(initializers = [PostgresTestExtension::class])
|
||||||
|
@TestExecutionListeners(listeners = [PostgresTestListener::class], mergeMode = MergeMode.MERGE_WITH_DEFAULTS)
|
||||||
|
annotation class PostgresTestContainer
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package ltd.hlaeja.test.container.extension
|
||||||
|
|
||||||
|
import ltd.hlaeja.test.container.kafka.TestContainerKafka
|
||||||
|
import ltd.hlaeja.test.container.postgres.TestContainerPostgres
|
||||||
|
import org.junit.jupiter.api.extension.BeforeAllCallback
|
||||||
|
import org.junit.jupiter.api.extension.ExtensionContext
|
||||||
|
import org.springframework.boot.test.util.TestPropertyValues
|
||||||
|
import org.springframework.context.ApplicationContextInitializer
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext
|
||||||
|
|
||||||
|
class KafkaPostgresTestExtension : BeforeAllCallback, ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||||
|
|
||||||
|
override fun initialize(applicationContext: ConfigurableApplicationContext) {
|
||||||
|
TestPropertyValues
|
||||||
|
.of(TestContainerPostgres.props() + TestContainerKafka.props())
|
||||||
|
.applyTo(applicationContext.environment)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeAll(context: ExtensionContext) {
|
||||||
|
if (!TestContainerPostgres.postgres.isRunning) {
|
||||||
|
TestContainerPostgres.postgres.start()
|
||||||
|
}
|
||||||
|
if (!TestContainerKafka.kafka.isRunning) {
|
||||||
|
TestContainerKafka.kafka.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ltd.hlaeja.test.container.extension
|
||||||
|
|
||||||
|
import ltd.hlaeja.test.container.kafka.TestContainerKafka
|
||||||
|
import org.junit.jupiter.api.extension.BeforeAllCallback
|
||||||
|
import org.junit.jupiter.api.extension.ExtensionContext
|
||||||
|
import org.springframework.boot.test.util.TestPropertyValues
|
||||||
|
import org.springframework.context.ApplicationContextInitializer
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext
|
||||||
|
|
||||||
|
class KafkaTestExtension : BeforeAllCallback, ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||||
|
|
||||||
|
override fun initialize(applicationContext: ConfigurableApplicationContext) {
|
||||||
|
TestPropertyValues.of(TestContainerKafka.props()).applyTo(applicationContext.environment)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeAll(context: ExtensionContext) {
|
||||||
|
if (!TestContainerKafka.kafka.isRunning) {
|
||||||
|
TestContainerKafka.kafka.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ltd.hlaeja.test.container.extension
|
||||||
|
|
||||||
|
import ltd.hlaeja.test.container.postgres.TestContainerPostgres
|
||||||
|
import org.junit.jupiter.api.extension.BeforeAllCallback
|
||||||
|
import org.junit.jupiter.api.extension.ExtensionContext
|
||||||
|
import org.springframework.boot.test.util.TestPropertyValues
|
||||||
|
import org.springframework.context.ApplicationContextInitializer
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext
|
||||||
|
|
||||||
|
class PostgresTestExtension : BeforeAllCallback, ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||||
|
|
||||||
|
override fun initialize(applicationContext: ConfigurableApplicationContext) {
|
||||||
|
TestPropertyValues.of(TestContainerPostgres.props()).applyTo(applicationContext.environment)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeAll(context: ExtensionContext) {
|
||||||
|
if (!TestContainerPostgres.postgres.isRunning) {
|
||||||
|
TestContainerPostgres.postgres.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package ltd.hlaeja.test.container.kafka
|
||||||
|
|
||||||
|
import org.testcontainers.kafka.ConfluentKafkaContainer
|
||||||
|
|
||||||
|
object TestContainerKafka {
|
||||||
|
|
||||||
|
val kafka: ConfluentKafkaContainer = ConfluentKafkaContainer("confluentinc/cp-kafka:8.0.0")
|
||||||
|
.withReuse(true)
|
||||||
|
|
||||||
|
fun props(): Map<String, String> = mapOf(
|
||||||
|
"spring.kafka.bootstrap-servers" to kafka.bootstrapServers,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package ltd.hlaeja.test.container.postgres
|
||||||
|
|
||||||
|
import ltd.hlaeja.test.container.postgres.TestContainerPostgres.sqlFile
|
||||||
|
import ltd.hlaeja.test.container.util.hasAnnotation
|
||||||
|
import org.junit.jupiter.api.Nested
|
||||||
|
import org.springframework.test.context.TestContext
|
||||||
|
import org.springframework.test.context.TestExecutionListener
|
||||||
|
|
||||||
|
class PostgresTestListener : TestExecutionListener {
|
||||||
|
|
||||||
|
override fun beforeTestClass(
|
||||||
|
context: TestContext,
|
||||||
|
) {
|
||||||
|
if (context.testClass.hasAnnotation<Nested>()) return
|
||||||
|
sqlFile("postgres/data.sql", context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun afterTestClass(
|
||||||
|
context: TestContext,
|
||||||
|
) {
|
||||||
|
if (context.testClass.hasAnnotation<Nested>()) return
|
||||||
|
sqlFile("postgres/reset.sql", context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package ltd.hlaeja.test.container.postgres
|
||||||
|
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import ltd.hlaeja.test.container.util.isResourceFile
|
||||||
|
import org.springframework.r2dbc.core.DatabaseClient
|
||||||
|
import org.springframework.r2dbc.core.await
|
||||||
|
import org.springframework.test.context.TestContext
|
||||||
|
import org.testcontainers.containers.PostgreSQLContainer
|
||||||
|
import org.testcontainers.utility.DockerImageName
|
||||||
|
|
||||||
|
object TestContainerPostgres {
|
||||||
|
|
||||||
|
val postgres = PostgreSQLContainer(DockerImageName.parse("postgres:17"))
|
||||||
|
.withReuse(true)
|
||||||
|
.apply {
|
||||||
|
withDatabaseName("testdb")
|
||||||
|
withUsername("test")
|
||||||
|
withPassword("test")
|
||||||
|
"postgres/schema.sql".isResourceFile()?.let { withInitScript(it.path) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun props(): Map<String, String> = postgres.let {
|
||||||
|
mapOf(
|
||||||
|
"spring.r2dbc.url" to "r2dbc:postgresql://${it.host}:${it.firstMappedPort}/${it.databaseName}",
|
||||||
|
"spring.r2dbc.username" to it.username,
|
||||||
|
"spring.r2dbc.password" to it.password,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sqlFile(
|
||||||
|
sqlFile: String,
|
||||||
|
context: TestContext,
|
||||||
|
): Unit = runBlocking {
|
||||||
|
sqlFile.isResourceFile()
|
||||||
|
?.inputStream
|
||||||
|
?.use {
|
||||||
|
executeSqlStatements(
|
||||||
|
makeSqlStatements(it),
|
||||||
|
context.applicationContext.getBean(DatabaseClient::class.java),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("TooGenericExceptionThrown", "SqlSourceToSinkFlow")
|
||||||
|
private suspend fun executeSqlStatements(
|
||||||
|
statements: List<String>,
|
||||||
|
databaseClient: DatabaseClient,
|
||||||
|
) = try {
|
||||||
|
statements.forEach { databaseClient.sql(it).await() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw RuntimeException("Failed to execute SQL statements", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun makeSqlStatements(
|
||||||
|
inputStream: InputStream,
|
||||||
|
): List<String> = BufferedReader(InputStreamReader(inputStream))
|
||||||
|
.lines()
|
||||||
|
.filter { it.isNotEmpty() && !it.startsWith("--") }
|
||||||
|
.map { it.trim() }
|
||||||
|
.toList()
|
||||||
|
.joinToString(" ")
|
||||||
|
.split(';')
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.map { "${it.trim()};" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package ltd.hlaeja.test.container.util
|
||||||
|
|
||||||
|
import org.springframework.core.io.ClassPathResource
|
||||||
|
|
||||||
|
fun String.isResourceFile(): ClassPathResource? = ClassPathResource(this).let { resource ->
|
||||||
|
when {
|
||||||
|
resource.exists() && resource.isReadable -> resource
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Annotation> Class<*>.hasAnnotation(): Boolean = getAnnotation(T::class.java) != null
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package ltd.hlaeja.test.util
|
|
||||||
|
|
||||||
import org.springframework.context.ConfigurableApplicationContext
|
|
||||||
import org.springframework.core.io.ClassPathResource
|
|
||||||
import org.springframework.test.context.TestContext
|
|
||||||
|
|
||||||
fun ConfigurableApplicationContext.getProperty(
|
|
||||||
property: String,
|
|
||||||
): String? = this.environment.getProperty(property)
|
|
||||||
|
|
||||||
fun ConfigurableApplicationContext.getProperty(
|
|
||||||
property: String,
|
|
||||||
default: String,
|
|
||||||
): String = this.environment.getProperty(property, default)
|
|
||||||
|
|
||||||
fun TestContext.getProperty(
|
|
||||||
property: String,
|
|
||||||
): String? = this.applicationContext.environment.getProperty(property)
|
|
||||||
|
|
||||||
fun String.isResourceFile(): ClassPathResource? {
|
|
||||||
val resource = ClassPathResource(this)
|
|
||||||
return when {
|
|
||||||
resource.exists() && resource.isReadable -> resource
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user