diff --git a/README.md b/README.md index 46951aa..bc6621e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,20 @@ Library to test integration for aura ascend. +## Postgres Test Container + +`@PostgresTestContainer` Annotation for integration tests. + +Initialize Postgres test container using test container default properties, script located in `src//resources/postgres/` folder. + +### Properties For Test Container + +| file | required | info | +|---------------------|:--------:|----------------------------------------------------------------| +| postgres/schema.sql | ✓ | Postgres init script containing all structure and functions | +| postgres/data.sql | | Postgres data script containing all data to populate database | +| postgres/reset.sql | | Postgres reset script containing all command to reset database | + ## Publish library locally. ```shell diff --git a/build.gradle.kts b/build.gradle.kts index 091aca2..c625981 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,14 @@ plugins { alias(aa.plugins.kotlin.jvm) + alias(aa.plugins.spring.dependency.management) alias(aa.plugins.library) } dependencies { - + implementation(aa.kotlinx.coroutines) + implementation(aa.springboot.starter.r2dbc) + implementation(aa.springboot.starter.test) + implementation(aa.testcontainers.postgresql) } group = "ltd.lulz.library" diff --git a/src/main/kotlin/ltd/lulz/test/container/PostgresTestContainer.kt b/src/main/kotlin/ltd/lulz/test/container/PostgresTestContainer.kt new file mode 100644 index 0000000..8fd22a6 --- /dev/null +++ b/src/main/kotlin/ltd/lulz/test/container/PostgresTestContainer.kt @@ -0,0 +1,15 @@ +package ltd.lulz.test.container + +import ltd.lulz.test.container.extension.PostgresTestExtension +import ltd.lulz.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.MERGE_WITH_DEFAULTS + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(PostgresTestExtension::class) +@ContextConfiguration(initializers = [PostgresTestExtension::class]) +@TestExecutionListeners(listeners = [PostgresTestListener::class], mergeMode = MERGE_WITH_DEFAULTS) +annotation class PostgresTestContainer diff --git a/src/main/kotlin/ltd/lulz/test/container/extension/PostgresTestExtension.kt b/src/main/kotlin/ltd/lulz/test/container/extension/PostgresTestExtension.kt new file mode 100644 index 0000000..cd5fd06 --- /dev/null +++ b/src/main/kotlin/ltd/lulz/test/container/extension/PostgresTestExtension.kt @@ -0,0 +1,21 @@ +package ltd.lulz.test.container.extension + +import ltd.lulz.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 { + + override fun initialize(applicationContext: ConfigurableApplicationContext) { + TestPropertyValues.of(TestContainerPostgres.props()).applyTo(applicationContext.environment) + } + + override fun beforeAll(context: ExtensionContext) { + if (!TestContainerPostgres.postgres.isRunning) { + TestContainerPostgres.postgres.start() + } + } +} diff --git a/src/main/kotlin/ltd/lulz/test/container/postgres/PostgresTestListener.kt b/src/main/kotlin/ltd/lulz/test/container/postgres/PostgresTestListener.kt new file mode 100644 index 0000000..d6277b0 --- /dev/null +++ b/src/main/kotlin/ltd/lulz/test/container/postgres/PostgresTestListener.kt @@ -0,0 +1,24 @@ +package ltd.lulz.test.container.postgres + +import ltd.lulz.test.container.postgres.TestContainerPostgres.sqlFile +import ltd.lulz.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()) return + sqlFile("postgres/data.sql", context) + } + + override fun afterTestClass( + context: TestContext, + ) { + if (context.testClass.hasAnnotation()) return + sqlFile("postgres/reset.sql", context) + } +} diff --git a/src/main/kotlin/ltd/lulz/test/container/postgres/TestContainerPostgres.kt b/src/main/kotlin/ltd/lulz/test/container/postgres/TestContainerPostgres.kt new file mode 100644 index 0000000..82520dc --- /dev/null +++ b/src/main/kotlin/ltd/lulz/test/container/postgres/TestContainerPostgres.kt @@ -0,0 +1,68 @@ +package ltd.lulz.test.container.postgres + +import java.io.BufferedReader +import java.io.InputStream +import java.io.InputStreamReader +import kotlinx.coroutines.runBlocking +import ltd.lulz.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:18rc1-alpine")) + .withReuse(true) + .apply { + withDatabaseName("testdb") + withUsername("test") + withPassword("test") + "postgres/schema.sql".isResourceFile()?.let { withInitScript(it.path) } + } + + fun props(): Map = 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, + 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 = BufferedReader(InputStreamReader(inputStream)) + .lines() + .filter { it.isNotEmpty() && !it.startsWith("--") } + .map { it.trim() } + .toList() + .joinToString(" ") + .split(';') + .filter { it.isNotBlank() } + .map { "${it.trim()};" } +} diff --git a/src/main/kotlin/ltd/lulz/test/container/util/ContainerUtil.kt b/src/main/kotlin/ltd/lulz/test/container/util/ContainerUtil.kt new file mode 100644 index 0000000..e0a0472 --- /dev/null +++ b/src/main/kotlin/ltd/lulz/test/container/util/ContainerUtil.kt @@ -0,0 +1,13 @@ +package ltd.lulz.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 Class<*>.hasAnnotation(): Boolean = getAnnotation(T::class.java) != null