From 3e5a2c435e55fbff89102ccba0f07fc2e78b80b2 Mon Sep 17 00:00:00 2001 From: Rodolfo Date: Wed, 4 Dec 2024 16:17:28 -0300 Subject: [PATCH] using TestContainers --- README.md | 21 +---- docker-compose.yml | 2 +- pom.xml | 31 ++++--- src/main/kotlin/crablet/Crablet.kt | 12 +-- .../crablet/postgres/CrabletEventsAppender.kt | 2 +- .../main/resources/ddl}/1-events_table.sql | 0 .../main/resources/ddl}/2-append_events.sql | 2 + .../kotlin/crablet/example/CrabletTest2.kt | 61 ++++++++++++++ .../crablet/postgres/AbstractCrabletTest.kt | 53 ++++++++++++ ...stgresTest.kt => Scenario1TestAbstract.kt} | 16 +--- .../crablet/postgres/Scenario2TestAbstract.kt | 84 +++++++++++++++++++ {sql => src/test/resources}/3-test.sql | 2 +- {sql => src/test/resources}/4-test.sql | 0 13 files changed, 226 insertions(+), 60 deletions(-) rename {sql => src/main/resources/ddl}/1-events_table.sql (100%) rename {sql => src/main/resources/ddl}/2-append_events.sql (98%) create mode 100644 src/test/kotlin/crablet/example/CrabletTest2.kt create mode 100644 src/test/kotlin/crablet/postgres/AbstractCrabletTest.kt rename src/test/kotlin/crablet/postgres/{CrabletPostgresTest.kt => Scenario1TestAbstract.kt} (85%) create mode 100644 src/test/kotlin/crablet/postgres/Scenario2TestAbstract.kt rename {sql => src/test/resources}/3-test.sql (96%) rename {sql => src/test/resources}/4-test.sql (100%) diff --git a/README.md b/README.md index a3e0c09..84bcbff 100644 --- a/README.md +++ b/README.md @@ -2,28 +2,9 @@ So far, just an experiment -## Running - -1. On terminal, run: - ```bash - docker-compose up - ``` - -2. Run the main method of [`CrabletTest1.kt`](src/test/kotlin/crablet/example/CrabletTest1.kt) - - ```text -Vertx Pool started -Append operation ---> eventsToAppend: [{"type":"AccountOpened","id":10}, {"type":"AmountDeposited","amount":100}] ---> appendCondition: AppendCondition(query=StreamQuery(identifiers=[DomainIdentifier(name=StateName(value=Account), id=StateId(value=51834a25-31b4-461d-8cfe-cc75a842bbcb))], eventTypes=[EventName(value=AccountOpened), EventName(value=AmountDeposited)]), maximumEventSequence=SequenceNumber(value=0)) - -New sequence id ---> SequenceNumber(value=12) -New state ---> [{"type":"AccountOpened","id":10},{"type":"AmountDeposited","amount":100}] - ``` - ## References +* https://github.com/crabzilla/crabzilla * https://www.youtube.com/watch?v=GzrZworHpIk * https://www.youtube.com/watch?v=DhhxKoOpJe0 -* https://github.com/crabzilla/crabzilla * https://github.com/imrafaelmerino/json-values \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7e4f1da..6bfee0a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,6 @@ services: POSTGRES_USER: postgres POSTGRES_DB: postgres volumes: - - ./sql:/docker-entrypoint-initdb.d + - ./ddl:/docker-entrypoint-initdb.d ports: - 5432:5432 diff --git a/pom.xml b/pom.xml index 6d9f19c..155e998 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ UTF-8 quarkus-bom io.quarkus.platform - 3.16.3 + 3.17.2 true 3.5.0 @@ -44,15 +44,14 @@ io.quarkus quarkus-reactive-pg-client - - io.quarkus - quarkus-reactive-routes - - - io.quarkus - quarkus-arc - - + + + + + + + + io.quarkus @@ -74,12 +73,12 @@ kotlin-extensions test - - - - - - + + org.testcontainers + postgresql + 1.19.8 + test + com.github.imrafaelmerino json-values diff --git a/src/main/kotlin/crablet/Crablet.kt b/src/main/kotlin/crablet/Crablet.kt index 578cf99..19a0dd4 100644 --- a/src/main/kotlin/crablet/Crablet.kt +++ b/src/main/kotlin/crablet/Crablet.kt @@ -21,12 +21,6 @@ data class DomainIdentifier(val name: StateName, val id: StateId) { data class StreamQuery(val identifiers: List, val eventTypes: List) -// read - -interface StateBuilder { - fun buildFor(query: StreamQuery): Future> -} - // write data class AppendCondition(val query: StreamQuery, val maximumEventSequence: SequenceNumber) @@ -34,3 +28,9 @@ data class AppendCondition(val query: StreamQuery, val maximumEventSequence: Seq interface EventsAppender { fun appendIf(events: List, appendCondition: AppendCondition): Future } + +// read + +interface StateBuilder { + fun buildFor(query: StreamQuery): Future> +} diff --git a/src/main/kotlin/crablet/postgres/CrabletEventsAppender.kt b/src/main/kotlin/crablet/postgres/CrabletEventsAppender.kt index 5769569..542ce66 100644 --- a/src/main/kotlin/crablet/postgres/CrabletEventsAppender.kt +++ b/src/main/kotlin/crablet/postgres/CrabletEventsAppender.kt @@ -36,7 +36,7 @@ class CrabletEventsAppender(private val client: Pool) : EventsAppender { .execute(params) }.onSuccess { rowSet -> // Extract the result (last_sequence_id) from the first row - val latestSequenceId = rowSet.firstOrNull()?.getLong("last_sequence_id") + val latestSequenceId = rowSet.first().getLong("last_sequence_id") if (latestSequenceId != null) { promise.complete(SequenceNumber(latestSequenceId)) } else { diff --git a/sql/1-events_table.sql b/src/main/resources/ddl/1-events_table.sql similarity index 100% rename from sql/1-events_table.sql rename to src/main/resources/ddl/1-events_table.sql diff --git a/sql/2-append_events.sql b/src/main/resources/ddl/2-append_events.sql similarity index 98% rename from sql/2-append_events.sql rename to src/main/resources/ddl/2-append_events.sql index 00c7183..12d7c13 100644 --- a/sql/2-append_events.sql +++ b/src/main/resources/ddl/2-append_events.sql @@ -66,6 +66,8 @@ BEGIN -- After the first insert, set causation_id to the sequence_id of the last event inserted causation_id := sequence_id; + currentLastSequence = sequence_id; + END LOOP; ELSE -- Raise an exception if sequence mismatch is detected diff --git a/src/test/kotlin/crablet/example/CrabletTest2.kt b/src/test/kotlin/crablet/example/CrabletTest2.kt new file mode 100644 index 0000000..b6447ba --- /dev/null +++ b/src/test/kotlin/crablet/example/CrabletTest2.kt @@ -0,0 +1,61 @@ +package crablet.example + +import crablet.AppendCondition +import crablet.DomainIdentifier +import crablet.EventName +import crablet.SequenceNumber +import crablet.StateId +import crablet.StateName +import crablet.StreamQuery +import crablet.postgres.CrabletEventsAppender +import crablet.postgres.CrabletStateBuilder +import io.vertx.core.json.JsonArray +import io.vertx.core.json.JsonObject +import java.util.* + +fun main() { + + println("Vertx Pool started") + + val eventsAppender = CrabletEventsAppender(pool) + + val stateBuilder = CrabletStateBuilder( + client = pool, + initialState = JsonArray(), + evolveFunction = { state, event -> state.add(event) }) + + val domainIdentifiers = listOf( + DomainIdentifier(name = StateName("Account"), id = StateId(UUID.randomUUID().toString())) + ) + + val streamQuery = StreamQuery( + identifiers = domainIdentifiers, + eventTypes = listOf("AccountOpened", "AmountDeposited").map { EventName(it) } + ) + + val appendCondition = AppendCondition(query = streamQuery, maximumEventSequence = SequenceNumber(0)) + + val eventsToAppend: List = listOf( + JsonObject().put("type", "AccountOpened").put("id", 10), + JsonObject().put("type", "AmountDeposited").put("amount", 100) + ) + + println("Append operation") + println("--> eventsToAppend: $eventsToAppend") + println("--> appendCondition: $appendCondition ") + + // append events + eventsAppender.appendIf(eventsToAppend, appendCondition) + .compose { + // print the resulting sequenceId + println() + println("New sequence id ---> $it") + // now project a state given the past events + stateBuilder.buildFor(streamQuery) + } + .onSuccess { stateResult: Pair -> + println("New state ---> ${stateResult.first}") + } + .onFailure { it.printStackTrace() } + +} diff --git a/src/test/kotlin/crablet/postgres/AbstractCrabletTest.kt b/src/test/kotlin/crablet/postgres/AbstractCrabletTest.kt new file mode 100644 index 0000000..bb77931 --- /dev/null +++ b/src/test/kotlin/crablet/postgres/AbstractCrabletTest.kt @@ -0,0 +1,53 @@ +package crablet.postgres + +import io.vertx.pgclient.PgConnectOptions +import io.vertx.sqlclient.Pool +import io.vertx.sqlclient.PoolOptions +import org.testcontainers.containers.PostgreSQLContainer +import org.testcontainers.utility.MountableFile + +abstract class AbstractCrabletTest { + + companion object { + + val PG_DOCKER_IMAGE = "postgres:latest" + val DB_NAME = "postgres" + val DB_USERNAME = "postgres" + val DB_PASSWORD = "postgres" + + val postgresqlContainer = + PostgreSQLContainer(PG_DOCKER_IMAGE) + .apply { + withDatabaseName(DB_NAME) + withUsername(DB_USERNAME) + withPassword(DB_PASSWORD) + withCopyFileToContainer( + MountableFile.forClasspathResource("./ddl/1-events_table.sql"), + "/docker-entrypoint-initdb.d/1-events_table.sql", + ) + withCopyFileToContainer( + MountableFile.forClasspathResource("./ddl/2-append_events.sql"), + "/docker-entrypoint-initdb.d/2-append_events.sql", + ) + withEnv("POSTGRES_LOG_CONNECTIONS", "on") + withEnv("POSTGRES_LOG_DISCONNECTIONS", "on") + withEnv("POSTGRES_LOG_DURATION", "on") + withEnv("POSTGRES_LOG_STATEMENT", "all") + withEnv("POSTGRES_LOG_DIRECTORY", "/var/log/pg_log") + withLogConsumer { frame -> println(frame.utf8String) } + } + + val pool: Pool by lazy { + postgresqlContainer.start() + val connectOptions = PgConnectOptions() + .setPort(if (postgresqlContainer.isRunning) postgresqlContainer.firstMappedPort else 5432) + .setHost("127.0.0.1") + .setDatabase(DB_NAME) + .setUser(DB_USERNAME) + .setPassword(DB_PASSWORD) + val poolOptions = PoolOptions().setMaxSize(5) + Pool.pool(connectOptions, poolOptions) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/crablet/postgres/CrabletPostgresTest.kt b/src/test/kotlin/crablet/postgres/Scenario1TestAbstract.kt similarity index 85% rename from src/test/kotlin/crablet/postgres/CrabletPostgresTest.kt rename to src/test/kotlin/crablet/postgres/Scenario1TestAbstract.kt index d2c662b..e7619b9 100644 --- a/src/test/kotlin/crablet/postgres/CrabletPostgresTest.kt +++ b/src/test/kotlin/crablet/postgres/Scenario1TestAbstract.kt @@ -11,9 +11,6 @@ import io.vertx.core.json.JsonArray import io.vertx.core.json.JsonObject import io.vertx.junit5.VertxExtension import io.vertx.junit5.VertxTestContext -import io.vertx.pgclient.PgConnectOptions -import io.vertx.sqlclient.Pool -import io.vertx.sqlclient.PoolOptions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertTrue @@ -23,7 +20,7 @@ import org.junit.jupiter.api.extension.ExtendWith import java.util.* @ExtendWith(VertxExtension::class) -class CrabletPostgresTest { +class Scenario1TestAbstract : AbstractCrabletTest() { lateinit var eventsAppender: CrabletEventsAppender lateinit var stateBuilder: CrabletStateBuilder @@ -33,7 +30,6 @@ class CrabletPostgresTest { @BeforeEach fun setUp(testContext: VertxTestContext) { - val pool = createPool() eventsAppender = CrabletEventsAppender(pool) stateBuilder = CrabletStateBuilder( client = pool, @@ -83,14 +79,4 @@ class CrabletPostgresTest { } } - private fun createPool(): Pool { - val connectOptions = PgConnectOptions() - .setPort(5432) - .setHost("127.0.0.1") - .setDatabase("postgres") - .setUser("postgres") - .setPassword("postgres") - val poolOptions = PoolOptions().setMaxSize(5) - return Pool.pool(connectOptions, poolOptions) - } } \ No newline at end of file diff --git a/src/test/kotlin/crablet/postgres/Scenario2TestAbstract.kt b/src/test/kotlin/crablet/postgres/Scenario2TestAbstract.kt new file mode 100644 index 0000000..2395bd8 --- /dev/null +++ b/src/test/kotlin/crablet/postgres/Scenario2TestAbstract.kt @@ -0,0 +1,84 @@ +package crablet.postgres + +import crablet.AppendCondition +import crablet.DomainIdentifier +import crablet.EventName +import crablet.SequenceNumber +import crablet.StateId +import crablet.StateName +import crablet.StreamQuery +import io.vertx.core.json.JsonObject +import io.vertx.junit5.VertxExtension +import io.vertx.junit5.VertxTestContext +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.util.* + +@ExtendWith(VertxExtension::class) +class Scenario2TestAbstract : AbstractCrabletTest() { + + data class Account(val id: Int? = null, val balance: Int = 0) + + lateinit var eventsAppender: CrabletEventsAppender + lateinit var stateBuilder: CrabletStateBuilder + lateinit var appendCondition: AppendCondition + lateinit var eventsToAppend: List + lateinit var streamQuery: StreamQuery + + @BeforeEach + fun setUp(testContext: VertxTestContext) { + eventsAppender = CrabletEventsAppender(pool) + stateBuilder = CrabletStateBuilder( + client = pool, + initialState = Account(), + evolveFunction = { state, event -> + when (event.getString("type")) { + "AccountOpened" -> state.copy(id = event.getInteger("id")) + "AmountDeposited" -> state.copy(balance = state.balance.plus(event.getInteger("amount"))) + else -> state + } + }) + + val domainIdentifiers = listOf( + DomainIdentifier(name = StateName("Account"), id = StateId(UUID.randomUUID().toString())) + ) + streamQuery = StreamQuery( + identifiers = domainIdentifiers, + eventTypes = listOf("AccountOpened", "AmountDeposited").map { EventName(it) } + ) + appendCondition = AppendCondition(query = streamQuery, maximumEventSequence = SequenceNumber(0)) + eventsToAppend = listOf( + JsonObject().put("type", "AccountOpened").put("id", 10), + JsonObject().put("type", "AmountDeposited").put("amount", 100) + ) + testContext.completeNow() + } + + @Test + fun testAppendAndBuildState(testContext: VertxTestContext) { + // Append events and build the state + eventsAppender.appendIf(eventsToAppend, appendCondition) + .compose { + stateBuilder.buildFor(streamQuery) + } + .onSuccess { (state, sequence): Pair -> + + assertNotNull(state) + assertNotNull(sequence) + + assertEquals(state.id, 10) + assertEquals(state.balance, 100) + + // Complete the test context indicating the test passed + testContext.completeNow() + } + .onFailure { it -> + // Fail the test context indicating the test failed + testContext.failNow(it) + } + } + +} \ No newline at end of file diff --git a/sql/3-test.sql b/src/test/resources/3-test.sql similarity index 96% rename from sql/3-test.sql rename to src/test/resources/3-test.sql index 30f97dd..b851b79 100644 --- a/sql/3-test.sql +++ b/src/test/resources/3-test.sql @@ -9,4 +9,4 @@ SELECT append_events( '{"type": "PasswordChanged", "user_id": "123", "password": "newpassword123"}', '{"type": "UserProfileUpdated", "user_id": "123", "profile": {"age": 30, "city": "New York"}}' ]::TEXT[] -- event payloads (as JSON string) - ) AS last_sequence_id; + ); diff --git a/sql/4-test.sql b/src/test/resources/4-test.sql similarity index 100% rename from sql/4-test.sql rename to src/test/resources/4-test.sql