diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 56a4abb8a..6fc3843bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -174,6 +174,19 @@ jobs: FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + flamingock-mongodb-reactive-auditstore: + needs: [ build ] + uses: ./.github/workflows/module-release-graalvm.yml + with: + module: flamingock-mongodb-reactive-auditstore + secrets: + FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} + FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} + FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} + FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + flamingock-couchbase-auditstore: needs: [ build ] uses: ./.github/workflows/module-release-graalvm.yml @@ -279,6 +292,19 @@ jobs: FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + flamingock-mongodb-reactive-externalsystem-api: + needs: [ build ] + uses: ./.github/workflows/module-release-graalvm.yml + with: + module: flamingock-mongodb-reactive-externalsystem-api + secrets: + FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} + FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} + FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} + FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + flamingock-mongodb-sync-targetsystem: needs: [ build ] uses: ./.github/workflows/module-release-graalvm.yml @@ -292,6 +318,19 @@ jobs: FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + flamingock-mongodb-reactive-targetsystem: + needs: [ build ] + uses: ./.github/workflows/module-release-graalvm.yml + with: + module: flamingock-mongodb-reactive-targetsystem + secrets: + FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} + FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} + FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} + FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + flamingock-mongodb-springdata-targetsystem: needs: [ build ] uses: ./.github/workflows/module-release-graalvm.yml @@ -305,6 +344,19 @@ jobs: FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + flamingock-mongodb-springdata-reactive-targetsystem: + needs: [ build ] + uses: ./.github/workflows/module-release-graalvm.yml + with: + module: flamingock-mongodb-springdata-reactive-targetsystem + secrets: + FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} + FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} + FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} + FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + flamingock-sql-externalsystem-api: needs: [ build ] uses: ./.github/workflows/module-release-graalvm.yml @@ -396,6 +448,19 @@ jobs: FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + flamingock-reactive-util: + needs: [ build ] + uses: ./.github/workflows/module-release-graalvm.yml + with: + module: flamingock-reactive-util + secrets: + FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} + FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} + FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} + FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + mongodb-util: needs: [ build ] uses: ./.github/workflows/module-release-graalvm.yml @@ -409,6 +474,32 @@ jobs: FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + mongodb-sync-util: + needs: [ build ] + uses: ./.github/workflows/module-release-graalvm.yml + with: + module: mongodb-sync-util + secrets: + FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} + FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} + FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} + FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + + mongodb-reactive-util: + needs: [ build ] + uses: ./.github/workflows/module-release-graalvm.yml + with: + module: mongodb-reactive-util + secrets: + FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} + FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} + FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} + FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + mongodb-test-kit: needs: [ build ] uses: ./.github/workflows/module-release-graalvm.yml @@ -422,6 +513,19 @@ jobs: FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + mongodb-reactive-test-kit: + needs: [ build ] + uses: ./.github/workflows/module-release-graalvm.yml + with: + module: mongodb-reactive-test-kit + secrets: + FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} + FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} + FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} + FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} + FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + dynamodb-util: needs: [ build ] uses: ./.github/workflows/module-release-graalvm.yml @@ -585,14 +689,18 @@ jobs: flamingock-community, flamingock-community-bom, flamingock-mongodb-sync-auditstore, + flamingock-mongodb-reactive-auditstore, flamingock-couchbase-auditstore, flamingock-dynamodb-auditstore, flamingock-importer, flamingock-springboot-integration, flamingock-nontransactional-targetsystem, flamingock-mongodb-externalsystem-api, + flamingock-mongodb-reactive-externalsystem-api, flamingock-mongodb-sync-targetsystem, + flamingock-mongodb-reactive-targetsystem, flamingock-mongodb-springdata-targetsystem, + flamingock-mongodb-springdata-reactive-targetsystem, flamingock-sql-externalsystem-api, flamingock-sql-targetsystem, flamingock-dynamodb-externalsystem-api, @@ -600,8 +708,12 @@ jobs: flamingock-couchbase-externalsystem-api, flamingock-couchbase-targetsystem, test-util, + flamingock-reactive-util, mongodb-util, + mongodb-sync-util, + mongodb-reactive-util, mongodb-test-kit, + mongodb-reactive-test-kit, dynamodb-util, dynamodb-test-kit, couchbase-util, diff --git a/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts b/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts index c7d1a8598..6a1973de3 100644 --- a/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts +++ b/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts @@ -24,6 +24,7 @@ val communityProjects = setOf( "flamingock-community-bom", "flamingock-community", "flamingock-mongodb-sync-auditstore", + "flamingock-mongodb-reactive-auditstore", "flamingock-couchbase-auditstore", "flamingock-dynamodb-auditstore", "flamingock-sql-auditstore", @@ -38,7 +39,9 @@ val pluginProjects = setOf( val targetSystemProjects = setOf( "flamingock-nontransactional-targetsystem", "flamingock-mongodb-sync-targetsystem", + "flamingock-mongodb-reactive-targetsystem", "flamingock-mongodb-springdata-targetsystem", + "flamingock-mongodb-springdata-reactive-targetsystem", "flamingock-sql-targetsystem", "flamingock-dynamodb-targetsystem", "flamingock-couchbase-targetsystem" @@ -46,6 +49,7 @@ val targetSystemProjects = setOf( val externalSystemProjects = setOf( "flamingock-mongodb-externalsystem-api", + "flamingock-mongodb-reactive-externalsystem-api", "flamingock-couchbase-externalsystem-api", "flamingock-dynamodb-externalsystem-api", "flamingock-sql-externalsystem-api" @@ -55,6 +59,9 @@ val utilProjects = setOf( "general-util", "test-util", "mongodb-util", + "mongodb-sync-util", + "mongodb-reactive-util", + "flamingock-reactive-util", "dynamodb-util", "couchbase-util" ) @@ -68,6 +75,7 @@ val legacyProjects = setOf( val testKitsProjects = setOf( "mongodb-test-kit", + "mongodb-reactive-test-kit", "dynamodb-test-kit", "sql-test-kit", "couchbase-test-kit" diff --git a/cloud/flamingock-cloud/build.gradle.kts b/cloud/flamingock-cloud/build.gradle.kts index cad2b6b01..d2432c887 100644 --- a/cloud/flamingock-cloud/build.gradle.kts +++ b/cloud/flamingock-cloud/build.gradle.kts @@ -9,12 +9,16 @@ dependencies { api(project(":core:target-systems:flamingock-couchbase-targetsystem")) api(project(":core:target-systems:flamingock-dynamodb-targetsystem")) api(project(":core:target-systems:flamingock-mongodb-externalsystem-api")) + api(project(":core:target-systems:flamingock-mongodb-reactive-externalsystem-api")) + api(project(":core:target-systems:flamingock-mongodb-reactive-targetsystem")) api(project(":core:target-systems:flamingock-mongodb-springdata-targetsystem")) + api(project(":core:target-systems:flamingock-mongodb-springdata-reactive-targetsystem")) api(project(":core:target-systems:flamingock-mongodb-sync-targetsystem")) api(project(":core:target-systems:flamingock-sql-targetsystem")) // Community api(project(":community:flamingock-couchbase-auditstore")) api(project(":community:flamingock-dynamodb-auditstore")) + api(project(":community:flamingock-mongodb-reactive-auditstore")) api(project(":community:flamingock-mongodb-sync-auditstore")) api(project(":community:flamingock-sql-auditstore")) @@ -41,4 +45,4 @@ java { toolchain { languageVersion.set(JavaLanguageVersion.of(8)) } -} \ No newline at end of file +} diff --git a/community/flamingock-community-bom/build.gradle.kts b/community/flamingock-community-bom/build.gradle.kts index f7a3eac00..feb6aa159 100644 --- a/community/flamingock-community-bom/build.gradle.kts +++ b/community/flamingock-community-bom/build.gradle.kts @@ -8,7 +8,9 @@ plugins { dependencies { constraints { // Add constraints for BOM managed modules + api("io.flamingock:flamingock-reactive-util:${version}") api("io.flamingock:flamingock-community:${version}") + api("io.flamingock:flamingock-mongodb-reactive-auditstore:$version") api("io.flamingock:flamingock-mongodb-sync-auditstore:$version") api("io.flamingock:flamingock-mongodb-springdata-auditstore:${version}") api("io.flamingock:flamingock-couchbase-auditstore:$version") @@ -35,6 +37,9 @@ dependencies { //mongodb api("io.flamingock:flamingock-mongodb-sync-template:${mongodbTemplateVersion}") api("io.flamingock:flamingock-mongodb-externalsystem-api:${version}") + api("io.flamingock:flamingock-mongodb-reactive-externalsystem-api:${version}") + api("io.flamingock:flamingock-mongodb-reactive-targetsystem:${version}") + api("io.flamingock:flamingock-mongodb-springdata-reactive-targetsystem:${version}") api("io.flamingock:flamingock-mongodb-sync-targetsystem:${version}") api("io.flamingock:flamingock-mongodb-springdata-targetsystem:${version}") diff --git a/community/flamingock-community/build.gradle.kts b/community/flamingock-community/build.gradle.kts index fd6b77f97..16413ac7f 100644 --- a/community/flamingock-community/build.gradle.kts +++ b/community/flamingock-community/build.gradle.kts @@ -8,12 +8,16 @@ dependencies { api(project(":core:target-systems:flamingock-couchbase-targetsystem")) api(project(":core:target-systems:flamingock-dynamodb-targetsystem")) api(project(":core:target-systems:flamingock-mongodb-externalsystem-api")) + api(project(":core:target-systems:flamingock-mongodb-reactive-externalsystem-api")) + api(project(":core:target-systems:flamingock-mongodb-reactive-targetsystem")) + api(project(":core:target-systems:flamingock-mongodb-springdata-reactive-targetsystem")) api(project(":core:target-systems:flamingock-mongodb-springdata-targetsystem")) api(project(":core:target-systems:flamingock-mongodb-sync-targetsystem")) api(project(":core:target-systems:flamingock-sql-targetsystem")) // Community api(project(":community:flamingock-couchbase-auditstore")) api(project(":community:flamingock-dynamodb-auditstore")) + api(project(":community:flamingock-mongodb-reactive-auditstore")) api(project(":community:flamingock-mongodb-sync-auditstore")) api(project(":community:flamingock-sql-auditstore")) } @@ -24,4 +28,4 @@ java { toolchain { languageVersion.set(JavaLanguageVersion.of(8)) } -} \ No newline at end of file +} diff --git a/community/flamingock-mongodb-reactive-auditstore/build.gradle.kts b/community/flamingock-mongodb-reactive-auditstore/build.gradle.kts new file mode 100644 index 000000000..e6315ed76 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/build.gradle.kts @@ -0,0 +1,33 @@ +import org.jetbrains.kotlin.gradle.utils.extendsFrom + +dependencies { + implementation(project(":utils:mongodb-util")) + implementation(project(":utils:mongodb-reactive-util")) + implementation(project(":utils:flamingock-reactive-util")) + implementation(project(":core:flamingock-core")) + + api(project(":core:target-systems:flamingock-mongodb-reactive-externalsystem-api")) + + compileOnly("org.mongodb:mongodb-driver-reactivestreams:4.0.0") + + testImplementation(project(":utils:test-util")) + testImplementation(project(":utils:mongodb-reactive-test-kit")) + testImplementation(project(":core:target-systems:flamingock-nontransactional-targetsystem")) + testImplementation(project(":core:target-systems:flamingock-mongodb-reactive-targetsystem")) + testImplementation("org.testcontainers:testcontainers-mongodb:2.0.2") + testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.2") + testImplementation("org.mockito:mockito-inline:4.11.0") + testImplementation("io.mongock:mongock-standalone:5.5.0") +} + +description = "MongoDB audit store implementation using reactive streams MongoDB driver" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} + +configurations.testImplementation { + extendsFrom(configurations.compileOnly.get()) +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStore.java b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStore.java new file mode 100644 index 000000000..52e6d6f84 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStore.java @@ -0,0 +1,179 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.externalsystem.mongodb.reactive.api.MongoDBReactiveExternalSystem; +import io.flamingock.internal.common.core.context.ContextResolver; +import io.flamingock.internal.common.core.error.FlamingockException; +import io.flamingock.internal.core.configuration.community.CommunityConfigurable; +import io.flamingock.internal.core.external.store.CommunityAuditStore; +import io.flamingock.internal.core.external.store.audit.community.CommunityAuditPersistence; +import io.flamingock.internal.core.external.store.lock.community.CommunityLockService; +import io.flamingock.internal.util.Constants; +import io.flamingock.internal.util.TimeService; +import io.flamingock.internal.util.id.RunnerId; +import io.flamingock.store.mongodb.reactive.internal.MongoDBReactiveAuditPersistence; +import io.flamingock.store.mongodb.reactive.internal.MongoDBReactiveLockService; + +import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME; +import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_LOCK_STORE_NAME; + +public class MongoDBReactiveAuditStore implements CommunityAuditStore { + + private final MongoDBReactiveExternalSystem mongoDBTargetSystem; + + protected RunnerId runnerId; + private CommunityConfigurable communityConfiguration; + private MongoDBReactiveAuditPersistence persistence; + private MongoDBReactiveLockService lockService; + private MongoDatabase database; + private String auditRepositoryName = DEFAULT_AUDIT_STORE_NAME; + private String lockRepositoryName = DEFAULT_LOCK_STORE_NAME; + private ReadConcern readConcern = ReadConcern.MAJORITY; + private ReadPreference readPreference = ReadPreference.primary(); + private WriteConcern writeConcern = WriteConcern.MAJORITY.withJournal(true); + private boolean autoCreate = true; + + private MongoDBReactiveAuditStore(MongoDBReactiveExternalSystem mongoDBTargetSystem) { + this.mongoDBTargetSystem = mongoDBTargetSystem; + } + + /** + * Creates a {@link MongoDBReactiveAuditStore} using the same MongoDB client and + * database configured in the given {@link MongoDBReactiveExternalSystem}. + *

+ * Only the underlying MongoDB instance (client + database name) is reused. + * No additional target-system configuration is carried over. + * + * @param targetSystem the target system from which to derive the client and database + * @return a new audit store bound to the same MongoDB instance as the target system + */ + public static MongoDBReactiveAuditStore from(MongoDBReactiveExternalSystem targetSystem) { + return new MongoDBReactiveAuditStore(targetSystem); + } + + @Override + public String getId() { + return Constants.DEFAULT_MONGODB_AUDIT_STORE; + } + + public MongoDBReactiveAuditStore withAuditRepositoryName(String auditRepositoryName) { + this.auditRepositoryName = auditRepositoryName; + return this; + } + + public MongoDBReactiveAuditStore withLockRepositoryName(String lockRepositoryName) { + this.lockRepositoryName = lockRepositoryName; + return this; + } + + public MongoDBReactiveAuditStore withReadConcern(ReadConcern readConcern) { + this.readConcern = readConcern; + return this; + } + + public MongoDBReactiveAuditStore withReadPreference(ReadPreference readPreference) { + this.readPreference = readPreference; + return this; + } + + public MongoDBReactiveAuditStore withWriteConcern(WriteConcern writeConcern) { + this.writeConcern = writeConcern; + return this; + } + + public MongoDBReactiveAuditStore withAutoCreate(boolean autoCreate) { + this.autoCreate = autoCreate; + return this; + } + + @Override + public void initialize(ContextResolver baseContext) { + runnerId = baseContext.getRequiredDependencyValue(RunnerId.class); + communityConfiguration = baseContext.getRequiredDependencyValue(CommunityConfigurable.class); + database = mongoDBTargetSystem.getMongoDatabase(); + this.validate(); + } + + @Override + public synchronized CommunityAuditPersistence getPersistence() { + if (persistence == null) { + persistence = new MongoDBReactiveAuditPersistence( + communityConfiguration, + database, + auditRepositoryName, + readConcern, + readPreference, + writeConcern, + autoCreate + ); + persistence.initialize(runnerId); + } + return persistence; + } + + @Override + public synchronized CommunityLockService getLockService() { + if (lockService == null) { + lockService = new MongoDBReactiveLockService( + database, + lockRepositoryName, + readConcern, + readPreference, + writeConcern, + TimeService.getDefault() + ); + lockService.initialize(autoCreate); + + } + return lockService; + } + + private void validate() { + + if (database == null) { + throw new FlamingockException("The 'database' instance is required."); + } + + if (auditRepositoryName == null || auditRepositoryName.trim().isEmpty()) { + throw new FlamingockException("The 'auditRepositoryName' property is required."); + } + + if (lockRepositoryName == null || lockRepositoryName.trim().isEmpty()) { + throw new FlamingockException("The 'lockRepositoryName' property is required."); + } + + if (auditRepositoryName.trim().equalsIgnoreCase(lockRepositoryName.trim())) { + throw new FlamingockException("The 'auditRepositoryName' and 'lockRepositoryName' properties must not be the same."); + } + + if (readConcern == null) { + throw new FlamingockException("The 'readConcern' property is required."); + } + + if (readPreference == null) { + throw new FlamingockException("The 'readPreference' property is required."); + } + + if (writeConcern == null) { + throw new FlamingockException("The 'writeConcern' property is required."); + } + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveConfiguration.java b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveConfiguration.java new file mode 100644 index 000000000..9c36202ad --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveConfiguration.java @@ -0,0 +1,163 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive; + +import com.mongodb.ReadConcernLevel; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import io.flamingock.internal.common.core.context.ContextResolver; +import io.flamingock.internal.common.mongodb.MongoDBAuditStoreConfiguration; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import static com.mongodb.assertions.Assertions.isTrue; + +public class MongoDBReactiveConfiguration extends MongoDBAuditStoreConfiguration { + private ReadConcernLevel readConcern = ReadConcernLevel.MAJORITY; + private WriteConcernLevel writeConcern = WriteConcernLevel.MAJORITY_WITH_JOURNAL; + private ReadPreferenceLevel readPreference = ReadPreferenceLevel.PRIMARY; + + public ReadConcernLevel getReadConcern() { + return readConcern; + } + + public void setReadConcern(ReadConcernLevel readConcern) { + this.readConcern = readConcern; + } + + public void setReadConcern(String readConcern) { + this.readConcern = ReadConcernLevel.fromString(readConcern); + } + + public WriteConcernLevel getWriteConcern() { + return writeConcern; + } + + public void setWriteConcern(WriteConcernLevel writeConcern) { + this.writeConcern = writeConcern; + } + + public ReadPreferenceLevel getReadPreference() { + return readPreference; + } + + public void setReadPreference(ReadPreferenceLevel readPreference) { + this.readPreference = readPreference; + } + + public WriteConcern getBuiltMongoDBWriteConcern() { + WriteConcern wc; + if (writeConcern.w instanceof String) { + wc = new WriteConcern((String) writeConcern.w).withJournal(writeConcern.journal); + } else { + wc = new WriteConcern((int) writeConcern.w).withJournal(writeConcern.journal); + } + return writeConcern.getwTimeoutMs() == null + ? wc + : wc.withWTimeout(writeConcern.getwTimeoutMs().toMillis(), TimeUnit.MILLISECONDS); + } + + public void mergeConfig(ContextResolver dependencyContext) { + dependencyContext.getPropertyAs("mongodb.autoCreate", Boolean.class) + .ifPresent(this::setAutoCreate); + dependencyContext.getPropertyAs("mongodb.auditRepositoryName", String.class) + .ifPresent(this::setAuditRepositoryName); + dependencyContext.getPropertyAs("mongodb.lockRepositoryName", String.class) + .ifPresent(this::setLockRepositoryName); + + dependencyContext.getPropertyAs("mongodb.readConcern", String.class) + .ifPresent(this::setReadConcern); + + dependencyContext.getPropertyAs("mongodb.writeConcern.w", Object.class) + .ifPresent(this.writeConcern::setW); + dependencyContext.getPropertyAs("mongodb.writeConcern.journal", Boolean.class) + .ifPresent(this.writeConcern::setJournal); + dependencyContext.getPropertyAs("mongodb.writeConcern.wTimeout", Duration.class) + .ifPresent(this.writeConcern::setwTimeoutMs); + + dependencyContext.getPropertyAs("mongodb.readPreference", ReadPreferenceLevel.class) + .ifPresent(this::setReadPreference); + } + + public enum ReadPreferenceLevel { + PRIMARY(ReadPreference.primary()), + PRIMARY_PREFERRED(ReadPreference.primaryPreferred()), + SECONDARY(ReadPreference.secondary()), + SECONDARY_PREFERRED(ReadPreference.secondaryPreferred()), + NEAREST(ReadPreference.nearest()); + + private final ReadPreference value; + + ReadPreferenceLevel(ReadPreference value) { + this.value = value; + } + + public ReadPreference getValue() { + return value; + } + } + + public static class WriteConcernLevel { + + public static final WriteConcernLevel MAJORITY_WITH_JOURNAL = new WriteConcernLevel( + WriteConcern.MAJORITY.getWString(), + Duration.ofSeconds(1), + true); + private Object w; + private Duration wTimeoutMs; + private Boolean journal; + + public WriteConcernLevel(String w, Duration wTimeoutMs, Boolean journal) { + this.w = w; + this.wTimeoutMs = wTimeoutMs; + this.journal = journal; + } + + public WriteConcernLevel() { + } + + public int getW() { + isTrue("w is an Integer", w != null && w instanceof Integer); + return (Integer) w; + } + + public String getStringW() { + isTrue("w is a String", w != null && w instanceof String); + return (String) w; + } + + public void setW(Object w) { + this.w = w; + } + + public Duration getwTimeoutMs() { + return wTimeoutMs; + } + + public void setwTimeoutMs(Duration wTimeoutMs) { + this.wTimeoutMs = wTimeoutMs; + } + + public Boolean isJournal() { + return journal; + } + + public void setJournal(Boolean journal) { + this.journal = journal; + } + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditPersistence.java b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditPersistence.java new file mode 100644 index 000000000..1e98c2368 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditPersistence.java @@ -0,0 +1,84 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.internal; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.core.configuration.community.CommunityConfigurable; +import io.flamingock.internal.core.external.store.audit.community.AbstractCommunityAuditPersistence; +import io.flamingock.internal.util.Result; +import io.flamingock.internal.util.id.RunnerId; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class MongoDBReactiveAuditPersistence extends AbstractCommunityAuditPersistence { + + private MongoDBReactiveAuditor auditor; + private final MongoDatabase database; + private final String auditCollectionName; + private final ReadConcern readConcern; + private final ReadPreference readPreference; + private final WriteConcern writeConcern; + private final boolean autoCreate; + + + public MongoDBReactiveAuditPersistence(CommunityConfigurable localConfiguration, + MongoDatabase database, + String auditCollectionName, + ReadConcern readConcern, + ReadPreference readPreference, + WriteConcern writeConcern, + boolean autoCreate) { + super(localConfiguration); + this.database = database; + this.auditCollectionName = auditCollectionName; + this.readConcern = readConcern; + this.readPreference = readPreference; + this.writeConcern = writeConcern; + this.autoCreate = autoCreate; + } + + @Override + protected void doInitialize(RunnerId runnerId) { + //Auditor + auditor = new MongoDBReactiveAuditor(database, auditCollectionName, readConcern, readPreference, writeConcern); + auditor.initialize(autoCreate); + } + + @Deprecated + @Override + public Set> getNonGuardedTypes() { + return new HashSet<>(Collections.singletonList(ClientSession.class)); + } + + @Override + public List getAuditHistory() { + return auditor.getAuditHistory(); + } + + @Override + public Result writeEntry(AuditEntry auditEntry) { + return auditor.writeEntry(auditEntry); + } + +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditor.java b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditor.java new file mode 100644 index 000000000..a95f4e0d9 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditor.java @@ -0,0 +1,106 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.internal; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.ReplaceOptions; +import com.mongodb.client.result.UpdateResult; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.core.audit.AuditReader; +import io.flamingock.internal.common.mongodb.CollectionInitializator; +import io.flamingock.internal.common.mongodb.MongoDBAuditMapper; +import io.flamingock.internal.common.mongodb.MongoDBReactiveCollectionHelper; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; +import io.flamingock.internal.core.external.store.audit.LifecycleAuditWriter; +import io.flamingock.internal.util.Result; +import io.flamingock.internal.util.log.FlamingockLoggerFactory; +import io.flamingock.reactive.util.PublisherSync; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.slf4j.Logger; + +import java.util.List; +import java.util.stream.Collectors; + +import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_CHANGE_ID; +import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_EXECUTION_ID; +import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_STATE; + +public class MongoDBReactiveAuditor implements LifecycleAuditWriter, AuditReader { + + private static final Logger logger = FlamingockLoggerFactory.getLogger("MongoDBReactiveAuditor"); + + private final MongoCollection collection; + private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document())); + + MongoDBReactiveAuditor(MongoDatabase database, + String collectionName, + ReadConcern readConcern, + ReadPreference readPreference, + WriteConcern writeConcern) { + this.collection = database.getCollection(collectionName) + .withReadConcern(readConcern) + .withReadPreference(readPreference) + .withWriteConcern(writeConcern); + } + + protected void initialize(boolean autoCreate) { + CollectionInitializator initializer = new CollectionInitializator<>( + new MongoDBReactiveCollectionHelper(collection), + () -> new MongoDBDocumentHelper(new Document()), + new String[]{KEY_EXECUTION_ID, KEY_CHANGE_ID, KEY_STATE} + ); + if (autoCreate) { + initializer.initialize(); + } else { + initializer.justValidateCollection(); + } + + } + + @Override + public Result writeEntry(AuditEntry auditEntry) { + Bson filter = Filters.and( + Filters.eq(KEY_EXECUTION_ID, auditEntry.getExecutionId()), + Filters.eq(KEY_CHANGE_ID, auditEntry.getChangeId()), + Filters.eq(KEY_STATE, auditEntry.getState().name()) + ); + + Document entryDocument = mapper.toDocument(auditEntry).getDocument(); + + UpdateResult result = PublisherSync.first( + collection.replaceOne(filter, entryDocument, new ReplaceOptions().upsert(true))); + logger.debug("SaveOrUpdate[{}] with result" + + "\n[upsertId:{}, matches: {}, modifies: {}, acknowledged: {}]", auditEntry, result.getUpsertedId(), result.getMatchedCount(), result.getModifiedCount(), result.wasAcknowledged()); + + return Result.OK(); + } + + + @Override + public List getAuditHistory() { + return PublisherSync.collect(collection.find()) + .stream() + .map(MongoDBDocumentHelper::new) + .map(mapper::fromDocument) + .collect(Collectors.toList()); + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveLockService.java b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveLockService.java new file mode 100644 index 000000000..a7e11257d --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveLockService.java @@ -0,0 +1,180 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.internal; + +import com.mongodb.DuplicateKeyException; +import com.mongodb.ErrorCategory; +import com.mongodb.MongoWriteException; +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.result.UpdateResult; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.internal.common.mongodb.CollectionInitializator; +import io.flamingock.internal.common.mongodb.MongoDBLockMapper; +import io.flamingock.internal.common.mongodb.MongoDBReactiveCollectionHelper; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; +import io.flamingock.internal.core.external.store.lock.LockAcquisition; +import io.flamingock.internal.core.external.store.lock.LockKey; +import io.flamingock.internal.core.external.store.lock.LockServiceException; +import io.flamingock.internal.core.external.store.lock.community.CommunityLockEntry; +import io.flamingock.internal.core.external.store.lock.community.CommunityLockService; +import io.flamingock.internal.util.TimeService; +import io.flamingock.internal.util.id.RunnerId; +import io.flamingock.reactive.util.PublisherSync; +import org.bson.Document; +import org.bson.conversions.Bson; + +import java.util.Date; + +import static io.flamingock.internal.core.external.store.lock.LockStatus.LOCK_HELD; +import static io.flamingock.internal.core.external.store.lock.community.CommunityLockEntryConstants.EXPIRES_AT_FIELD; +import static io.flamingock.internal.core.external.store.lock.community.CommunityLockEntryConstants.KEY_FIELD; +import static io.flamingock.internal.core.external.store.lock.community.CommunityLockEntryConstants.OWNER_FIELD; +import static io.flamingock.internal.core.external.store.lock.community.CommunityLockEntryConstants.STATUS_FIELD; + +public class MongoDBReactiveLockService implements CommunityLockService { + + private final MongoDBLockMapper mapper = new MongoDBLockMapper<>(() -> new MongoDBDocumentHelper(new Document())); + + + private final MongoCollection collection; + private final TimeService timeService; + + public MongoDBReactiveLockService(MongoDatabase database, + String collectionName, + ReadConcern readConcern, + ReadPreference readPreference, + WriteConcern writeConcern, + TimeService timeService) { + this(database.getCollection(collectionName), + readConcern, + readPreference, + writeConcern, + timeService); + } + + protected MongoDBReactiveLockService(MongoCollection collection, + ReadConcern readConcern, + ReadPreference readPreference, + WriteConcern writeConcern, + TimeService timeService) { + this.collection = collection + .withReadConcern(readConcern) + .withReadPreference(readPreference) + .withWriteConcern(writeConcern); + this.timeService = timeService; + } + + public void initialize(boolean autoCreate) { + CollectionInitializator initializer = new CollectionInitializator<>( + new MongoDBReactiveCollectionHelper(collection), + () -> new MongoDBDocumentHelper(new Document()), + new String[]{KEY_FIELD} + ); + if (autoCreate) { + initializer.initialize(); + } else { + initializer.justValidateCollection(); + } + } + + + @Override + public LockAcquisition upsert(LockKey key, RunnerId owner, long leaseMillis) { + CommunityLockEntry newLock = new CommunityLockEntry(key.toString(), LOCK_HELD, owner.toString(), timeService.currentDatePlusMillis(leaseMillis)); + insertUpdate(newLock, false); + return new LockAcquisition(owner, leaseMillis); + } + + @Override + public LockAcquisition extendLock(LockKey key, RunnerId owner, long leaseMillis) throws LockServiceException { + CommunityLockEntry newLock = new CommunityLockEntry(key.toString(), LOCK_HELD, owner.toString(), timeService.currentDatePlusMillis(leaseMillis)); + insertUpdate(newLock, true); + return new LockAcquisition(owner, leaseMillis); + } + + @Override + public LockAcquisition getLockInfo(LockKey lockKey) { + Document result = PublisherSync.first( + collection.find(new Document().append(KEY_FIELD, lockKey.toString())).first()); + if (result != null) { + return mapper.fromDocument(new MongoDBDocumentHelper(result)); + } + return null; + } + + @Override + public void releaseLock(LockKey lockKey, RunnerId owner) { + PublisherSync.first(collection.deleteMany( + Filters.and(Filters.eq(KEY_FIELD, lockKey.toString()), Filters.eq(OWNER_FIELD, owner.toString())))); + } + + protected void insertUpdate(CommunityLockEntry newLock, boolean onlyIfSameOwner) { + boolean lockHeld; + String debErrorDetail = "not db error"; + Bson acquireLockQuery = getAcquireLockQuery(newLock.getKey(), newLock.getOwner(), onlyIfSameOwner); + Document lockDocument = mapper.toDocument(newLock).getDocument(); + Document newLockDocumentSet = new Document().append("$set", lockDocument); + try { + UpdateResult result = PublisherSync.first(collection.updateMany( + acquireLockQuery, + newLockDocumentSet, + new UpdateOptions().upsert(!onlyIfSameOwner))); + if (result == null) { + throw new LockServiceException( + acquireLockQuery.toString(), + newLockDocumentSet.toString(), + "MongoDB updateMany operation completed without returning an UpdateResult" + ); + } + lockHeld = result.getModifiedCount() <= 0 && result.getUpsertedId() == null; + + } catch (MongoWriteException ex) { + lockHeld = ex.getError().getCategory() == ErrorCategory.DUPLICATE_KEY; + + if (!lockHeld) { + throw ex; + } + debErrorDetail = ex.getError().toString(); + + } catch (DuplicateKeyException ex) { + lockHeld = true; + debErrorDetail = ex.getMessage(); + } + + if (lockHeld) { + throw new LockServiceException( + acquireLockQuery.toString(), + newLockDocumentSet.toString(), + debErrorDetail + ); + } + } + + protected Bson getAcquireLockQuery(String lockKey, String owner, boolean onlyIfSameOwner) { + Bson expirationCond = Filters.lt(EXPIRES_AT_FIELD, new Date()); + Bson ownerCond = Filters.eq(OWNER_FIELD, owner); + Bson keyCond = Filters.eq(KEY_FIELD, lockKey); + Bson statusCond = Filters.eq(STATUS_FIELD, LOCK_HELD.name()); + return onlyIfSameOwner + ? Filters.and(keyCond, statusCond, ownerCond) + : Filters.and(keyCond, Filters.or(expirationCond, ownerCond)); + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/resources/META-INF/services/io.flamingock.internal.core.external.store.CommunityAuditStore b/community/flamingock-mongodb-reactive-auditstore/src/main/resources/META-INF/services/io.flamingock.internal.core.external.store.CommunityAuditStore new file mode 100644 index 000000000..199802661 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/main/resources/META-INF/services/io.flamingock.internal.core.external.store.CommunityAuditStore @@ -0,0 +1 @@ +io.flamingock.store.mongodb.reactive.MongoDBReactiveAuditStore diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditPersistenceE2ETest.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditPersistenceE2ETest.java new file mode 100644 index 000000000..613eb3064 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditPersistenceE2ETest.java @@ -0,0 +1,358 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive; + +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.common.test.pipeline.CodeChangeTestDefinition; +import io.flamingock.store.mongodb.reactive.changes.audit._003__NonTxTargetSystemChange; +import io.flamingock.store.mongodb.reactive.changes.audit._001__NonTxTransactionalFalseChange; +import io.flamingock.store.mongodb.reactive.changes.audit._004__TxSeparateAndSameMongoClientChange; +import io.flamingock.store.mongodb.reactive.changes.audit._005__TxSeparateChange; +import io.flamingock.store.mongodb.reactive.changes.audit._002__TxSharedDefaultChange; +import io.flamingock.core.kit.TestKit; +import io.flamingock.core.kit.audit.AuditTestHelper; +import io.flamingock.core.kit.audit.AuditTestSupport; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.core.audit.AuditTxType; +import io.flamingock.targetsystem.nontransactional.NonTransactionalTargetSystem; +import io.flamingock.mongodb.reactive.kit.MongoDBReactiveTestKit; +import io.flamingock.targetsystem.mongodb.reactive.MongoDBReactiveTargetSystem; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.time.LocalDateTime; +import java.util.Collections; + +import static io.flamingock.core.kit.audit.AuditEntryExpectation.APPLIED; +import static io.flamingock.core.kit.audit.AuditEntryExpectation.STARTED; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@Testcontainers +class MongoDBReactiveAuditPersistenceE2ETest { + + @Container + static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.4.6")); + + private TestKit testKit; + private AuditTestHelper auditHelper; + private MongoClient sharedMongoClient; + private MongoClient separateMongoClient; + private MongoDatabase database; + + @BeforeEach + void setUp() { + // Create shared MongoClient for audit storage and TX_SHARED scenarios + sharedMongoClient = MongoClients.create(mongoDBContainer.getConnectionString()); + database = sharedMongoClient.getDatabase("test"); + + // Create separate MongoClient for TX_SEPARATE_NO_MARKER scenarios + separateMongoClient = MongoClients.create(mongoDBContainer.getConnectionString()); + + // Initialize test kit with MongoDB persistence + testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test")), sharedMongoClient, database); + auditHelper = testKit.getAuditHelper(); + } + + @AfterEach + void tearDown() { + testKit.cleanUp(); + } + + @Test + @DisplayName("Should persist all audit entry fields correctly in MongoDB") + void testCompleteAuditEntryPersistenceInMongoDB() { + // Given + String changeId = "non-tx-transactional-false"; + LocalDateTime testStart = LocalDateTime.now(); + LocalDateTime testEnd = testStart.plusMinutes(5); // Allow enough time for test execution + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test"); + + // Given-When-Then - Test MongoDB audit persistence with AuditTestSupport + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_001__NonTxTransactionalFalseChange.class, Collections.emptyList()) + ) + .WHEN(() -> { + assertDoesNotThrow(() -> { + testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .build() + .run(); + }); + }) + .THEN_VerifyAuditSequenceStrict( + STARTED(changeId) + .withType(AuditEntry.ChangeType.STANDARD_CODE) + .withClass(_001__NonTxTransactionalFalseChange.class) + .withTxType(AuditTxType.NON_TX) + .withTargetSystemId("mongodb") + .withSystemChange(false) + .withTimestampBetween(testStart, testEnd), + APPLIED(changeId) + .withType(AuditEntry.ChangeType.STANDARD_CODE) + .withClass(_001__NonTxTransactionalFalseChange.class) + .withTxType(AuditTxType.NON_TX) + .withTargetSystemId("mongodb") + .withSystemChange(false) + .withTimestampBetween(testStart, testEnd) + ) + .run(); + } + + @Test + @DisplayName("Should persist NON_TX txStrategy for transactional=false scenarios") + void testNonTxScenarios() { + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test"); + // Given-When-Then - Test NON_TX scenarios with AuditTestSupport + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_001__NonTxTransactionalFalseChange.class, Collections.emptyList()), + new CodeChangeTestDefinition(_003__NonTxTargetSystemChange.class, Collections.emptyList()) + ) + .WHEN(() -> { + assertDoesNotThrow(() -> { + testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .addTargetSystem(new NonTransactionalTargetSystem("non-tx-system")) // Non-transactional target system + .build() + .run(); + }); + }) + .THEN_VerifyAuditSequenceStrict( + // First change (NonTxTransactionalFalseChange) - STARTED & EXECUTED + STARTED("non-tx-transactional-false") + .withType(AuditEntry.ChangeType.STANDARD_CODE) + .withClass(_001__NonTxTransactionalFalseChange.class) + .withTxType(AuditTxType.NON_TX) + .withTargetSystemId("mongodb"), + APPLIED("non-tx-transactional-false") + .withType(AuditEntry.ChangeType.STANDARD_CODE) + .withClass(_001__NonTxTransactionalFalseChange.class) + .withTxType(AuditTxType.NON_TX) + .withTargetSystemId("mongodb"), + + // Second change (NonTxTargetSystemChange) - STARTED & EXECUTED + STARTED("non-tx-target-system") + .withType(AuditEntry.ChangeType.STANDARD_CODE) + .withClass(_003__NonTxTargetSystemChange.class) + .withTxType(AuditTxType.NON_TX) + .withTargetSystemId("non-tx-system"), + APPLIED("non-tx-target-system") + .withType(AuditEntry.ChangeType.STANDARD_CODE) + .withClass(_003__NonTxTargetSystemChange.class) + .withTxType(AuditTxType.NON_TX) + .withTargetSystemId("non-tx-system") + ) + .run(); + } + + @Test + @DisplayName("Should persist TX_SHARED txStrategy when targetSystem not defined in change") + void testTxSharedScenarios() { + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test"); + MongoDBReactiveTargetSystem sharedTargetSystem = new MongoDBReactiveTargetSystem("tx-shared-system", sharedMongoClient, "test"); // Same MongoClient as audit storage + + + // Given-When-Then - Test TX_SHARED scenarios with AuditTestSupport + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_002__TxSharedDefaultChange.class, Collections.emptyList()) + ) + .WHEN(() -> { + assertDoesNotThrow(() -> { + testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .addTargetSystem(sharedTargetSystem) + .build() + .run(); + }); + }) + .THEN_VerifyAuditSequenceStrict( + STARTED("tx-shared-default") + .withType(AuditEntry.ChangeType.STANDARD_CODE) + .withClass(_002__TxSharedDefaultChange.class) + .withTxType(AuditTxType.TX_SEPARATE_NO_MARKER) + .withTargetSystemId("mongodb"), + APPLIED("tx-shared-default") + .withType(AuditEntry.ChangeType.STANDARD_CODE) + .withClass(_002__TxSharedDefaultChange.class) + .withTxType(AuditTxType.TX_SEPARATE_NO_MARKER) + .withTargetSystemId("mongodb") + ) + .run(); + } + + @Test + @DisplayName("Should persist TX_SEPARATE_NO_MARKER when targetSystem defined and different from auditStore") + void testTxNoMarkerWhenSameMongoClientButDifferentTargetSystem() { + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test"); + MongoDBReactiveTargetSystem sharedTargetSystem = new MongoDBReactiveTargetSystem("mongo-system", sharedMongoClient, "test"); // Same MongoClient as audit storage + + // Given-When-Then - Test TX_SEPARATE_NO_MARKER scenarios with AuditTestSupport + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_004__TxSeparateAndSameMongoClientChange.class, Collections.emptyList()) + ) + .WHEN(() -> { + assertDoesNotThrow(() -> { + testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .addTargetSystem(sharedTargetSystem) + .build() + .run(); + }); + }) + .THEN_VerifyAuditSequenceStrict( + STARTED("tx-separate-no-marker") + .withType(AuditEntry.ChangeType.STANDARD_CODE) + .withClass(_004__TxSeparateAndSameMongoClientChange.class) + .withTxType(AuditTxType.TX_SEPARATE_NO_MARKER) + .withTargetSystemId("mongo-system"), + APPLIED("tx-separate-no-marker") + .withType(AuditEntry.ChangeType.STANDARD_CODE) + .withClass(_004__TxSeparateAndSameMongoClientChange.class) + .withTxType(AuditTxType.TX_SEPARATE_NO_MARKER) + .withTargetSystemId("mongo-system") + ) + .run(); + } + + @Test + @DisplayName("Should persist TX_SEPARATE_NO_MARKER txStrategy for different MongoClient scenario") + void testTxSeparateNoMarkerScenario() { + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test"); + MongoDatabase separateDatabase = separateMongoClient.getDatabase("test"); + MongoDBReactiveTargetSystem separateTargetSystem = new MongoDBReactiveTargetSystem("tx-separate-system", separateMongoClient, "test"); // Different MongoClient from audit storage + + // Given-When-Then - Test TX_SEPARATE_NO_MARKER scenarios with AuditTestSupport + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_005__TxSeparateChange.class, Collections.emptyList()) + ) + .WHEN(() -> { + assertDoesNotThrow(() -> { + testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .addTargetSystem(separateTargetSystem) + .build() + .run(); + }); + }) + .THEN_VerifyAuditSequenceStrict( + STARTED("tx-separate-no-marker") + .withType(AuditEntry.ChangeType.STANDARD_CODE) + .withClass(_005__TxSeparateChange.class) + .withTxType(AuditTxType.TX_SEPARATE_NO_MARKER) + .withTargetSystemId("tx-separate-system"), + APPLIED("tx-separate-no-marker") + .withType(AuditEntry.ChangeType.STANDARD_CODE) + .withClass(_005__TxSeparateChange.class) + .withTxType(AuditTxType.TX_SEPARATE_NO_MARKER) + .withTargetSystemId("tx-separate-system") + ) + .run(); + } + + @Test + @DisplayName("Should persist correct targetSystemId for different target system configurations") + void testTargetSystemIdVariations() { + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test"); + MongoDatabase separateDatabase = separateMongoClient.getDatabase("test"); + + // Given-When-Then - Test multiple target system configurations with AuditTestSupport + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_002__TxSharedDefaultChange.class, Collections.emptyList()), + new CodeChangeTestDefinition(_003__NonTxTargetSystemChange.class, Collections.emptyList()), + new CodeChangeTestDefinition(_005__TxSeparateChange.class, Collections.emptyList()) + ) + .WHEN(() -> { + assertDoesNotThrow(() -> { + testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .addTargetSystem(new NonTransactionalTargetSystem("non-tx-system")) + .addTargetSystem(new MongoDBReactiveTargetSystem("tx-separate-system", separateMongoClient, "test")) + .build() + .run(); + }); + }) + .THEN_VerifyAuditSequenceStrict( + // TxSharedDefaultChange - STARTED & EXECUTED with mongodb target system + STARTED("tx-shared-default").withTargetSystemId("mongodb"), + APPLIED("tx-shared-default").withTargetSystemId("mongodb"), + + // NonTxTargetSystemChange - STARTED & EXECUTED + STARTED("non-tx-target-system").withTargetSystemId("non-tx-system"), + APPLIED("non-tx-target-system").withTargetSystemId("non-tx-system"), + + // TxSeparateChange - STARTED & EXECUTED with separate target system + STARTED("tx-separate-no-marker").withTargetSystemId("tx-separate-system"), + APPLIED("tx-separate-no-marker").withTargetSystemId("tx-separate-system") + ) + .run(); + } + + @Test + @DisplayName("Should persist multiple changes with different txStrategy configurations correctly") + void testMultipleChangesWithDifferentConfigurations() { + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test"); + + + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_001__NonTxTransactionalFalseChange.class, Collections.emptyList()), + new CodeChangeTestDefinition(_002__TxSharedDefaultChange.class, Collections.emptyList()), + new CodeChangeTestDefinition(_005__TxSeparateChange.class, Collections.emptyList()) + ).WHEN(() -> assertDoesNotThrow(() -> { + MongoDatabase separateDatabase = separateMongoClient.getDatabase("test"); + testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .addTargetSystem(new MongoDBReactiveTargetSystem("tx-separate-system", separateMongoClient, "test")) + .build() + .run(); + })) + .THEN_VerifyAuditSequenceStrict( + STARTED("non-tx-transactional-false").withTxType(AuditTxType.NON_TX), + APPLIED("non-tx-transactional-false").withTxType(AuditTxType.NON_TX), + + // TxSharedDefaultChange - STARTED & EXECUTED + STARTED("tx-shared-default").withTxType(AuditTxType.TX_SEPARATE_NO_MARKER), + APPLIED("tx-shared-default").withTxType(AuditTxType.TX_SEPARATE_NO_MARKER), + + // TxSeparateChange - STARTED & EXECUTED + STARTED("tx-separate-no-marker").withTxType(AuditTxType.TX_SEPARATE_NO_MARKER), + APPLIED("tx-separate-no-marker").withTxType(AuditTxType.TX_SEPARATE_NO_MARKER) + ) + .run(); + + + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStoreTest.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStoreTest.java new file mode 100644 index 000000000..dd052af4c --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStoreTest.java @@ -0,0 +1,211 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive; + +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.common.test.pipeline.CodeChangeTestDefinition; +import io.flamingock.store.mongodb.reactive.changes._001__create_client_collection_happy; +import io.flamingock.store.mongodb.reactive.changes._002__insert_federico_happy_non_transactional; +import io.flamingock.store.mongodb.reactive.changes._002__insert_federico_happy_transactional; +import io.flamingock.store.mongodb.reactive.changes._003__insert_jorge_failed_transactional_non_rollback; +import io.flamingock.store.mongodb.reactive.changes._003__insert_jorge_happy_transactional; +import io.flamingock.core.kit.TestKit; +import io.flamingock.core.kit.audit.AuditTestHelper; +import io.flamingock.core.kit.audit.AuditTestSupport; +import io.flamingock.internal.core.operation.OperationException; +import io.flamingock.mongodb.reactive.kit.MongoDBReactiveTestKit; +import io.flamingock.targetsystem.mongodb.reactive.MongoDBReactiveTargetSystem; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import static io.flamingock.core.kit.audit.AuditEntryExpectation.APPLIED; +import static io.flamingock.core.kit.audit.AuditEntryExpectation.FAILED; +import static io.flamingock.core.kit.audit.AuditEntryExpectation.ROLLED_BACK; +import static io.flamingock.core.kit.audit.AuditEntryExpectation.STARTED; +import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME; +import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_LOCK_STORE_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +class MongoDBReactiveAuditStoreTest { + + private static final String DB_NAME = "test"; + + private static final String CLIENTS_COLLECTION = "clientCollection"; + + private static final String CUSTOM_AUDIT_REPOSITORY_NAME = "testFlamingockAudit"; + private static final String CUSTOM_LOCK_REPOSITORY_NAME = "testFlamingockLock"; + + private static MongoClient mongoClient; + + private static MongoDatabase database; + + @Deprecated + private static MongoDBTestHelper mongoDBTestHelper; + + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")) + .withReuse(true); + private TestKit testKit; + private AuditTestHelper auditHelper; + + + @BeforeEach + void setupEach() { + mongoClient = MongoClients.create(mongoDBContainer.getConnectionString()); + database = mongoClient.getDatabase("test"); + testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test")), mongoClient, database); + auditHelper = testKit.getAuditHelper(); + + mongoDBTestHelper = new MongoDBTestHelper(database); + + } + + @AfterEach + void tearDown() { + ReactiveMongoTestHelper.complete(database.drop()); // Clean between tests + mongoClient.close(); + } + + @Test + @DisplayName("When standalone runs the AuditStore with DEFAULT repository names related collections should exists") + void happyPathWithDefaultRepositoryNames() { + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test"); + //Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.singletonList(MongoDatabase.class)), + new CodeChangeTestDefinition(_002__insert_federico_happy_transactional.class, Arrays.asList(MongoDatabase.class, ClientSession.class)), + new CodeChangeTestDefinition(_003__insert_jorge_happy_transactional.class, Arrays.asList(MongoDatabase.class, ClientSession.class)) + ) + .WHEN(() -> testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .build() + .run()) + .THEN_VerifyAuditSequenceStrict( + STARTED("create-client-collection"), + APPLIED("create-client-collection"), + STARTED("insert-federico-document"), + APPLIED("insert-federico-document"), + STARTED("insert-jorge-document"), + APPLIED("insert-jorge-document") + ) + .run(); + + assertTrue(mongoDBTestHelper.collectionExists(DEFAULT_AUDIT_STORE_NAME)); + assertTrue(mongoDBTestHelper.collectionExists(DEFAULT_LOCK_STORE_NAME)); + assertFalse(mongoDBTestHelper.collectionExists(CUSTOM_AUDIT_REPOSITORY_NAME)); + assertFalse(mongoDBTestHelper.collectionExists(CUSTOM_LOCK_REPOSITORY_NAME)); + } + + @Test + @DisplayName("When standalone runs the AuditStore with transactions enabled should persist the audit logs and the user's collection updated") + void happyPathWithTransaction() { + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test"); + //Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.singletonList(MongoDatabase.class)), + new CodeChangeTestDefinition(_002__insert_federico_happy_transactional.class, Arrays.asList(MongoDatabase.class, ClientSession.class)), + new CodeChangeTestDefinition(_003__insert_jorge_happy_transactional.class, Arrays.asList(MongoDatabase.class, ClientSession.class)) + ) + .WHEN(() -> testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .build() + .run()) + .THEN_VerifyAuditSequenceStrict( + STARTED("create-client-collection"), + APPLIED("create-client-collection"), + STARTED("insert-federico-document"), + APPLIED("insert-federico-document"), + STARTED("insert-jorge-document"), + APPLIED("insert-jorge-document") + ) + .run(); + + //Checking clients collection + Set clients = ReactiveMongoTestHelper.collect(database.getCollection(CLIENTS_COLLECTION) + .find()) + .stream() + .map(document -> document.getString("name")) + .collect(Collectors.toCollection(HashSet::new)); + assertEquals(2, clients.size()); + assertTrue(clients.contains("Federico")); + assertTrue(clients.contains("Jorge")); + } + + @Test + @DisplayName("When standalone runs the AuditStore with transactions enabled and execution fails should persist only the applied audit logs") + void failedWithTransaction() { + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test"); + //Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.singletonList(MongoDatabase.class)), + new CodeChangeTestDefinition(_002__insert_federico_happy_non_transactional.class, Collections.singletonList(MongoDatabase.class)), + new CodeChangeTestDefinition(_003__insert_jorge_failed_transactional_non_rollback.class, Arrays.asList(MongoDatabase.class, ClientSession.class)) + ) + .WHEN(() -> assertThrows(OperationException.class, () -> { + testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .build() + .run(); + })) + .THEN_VerifyAuditSequenceStrict( + STARTED("create-client-collection"), + APPLIED("create-client-collection"), + STARTED("insert-federico-document"), + APPLIED("insert-federico-document"), + STARTED("insert-jorge-document"), + FAILED("insert-jorge-document"), + ROLLED_BACK("insert-jorge-document") + ) + .run(); + + //Checking clients collection + Set clients = ReactiveMongoTestHelper.collect(database.getCollection(CLIENTS_COLLECTION) + .find()) + .stream() + .map(document -> document.getString("name")) + .collect(Collectors.toCollection(HashSet::new)); + assertEquals(1, clients.size()); + assertTrue(clients.contains("Federico")); + } + + +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveCoreStrategiesE2ETest.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveCoreStrategiesE2ETest.java new file mode 100644 index 000000000..53b263467 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveCoreStrategiesE2ETest.java @@ -0,0 +1,245 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive; + +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.common.test.pipeline.CodeChangeTestDefinition; +import io.flamingock.targetsystem.mongodb.reactive.MongoDBReactiveTargetSystem; +import io.flamingock.store.mongodb.reactive.changes._001__create_client_collection_happy; +import io.flamingock.store.mongodb.reactive.changes._002__insert_federico_happy_non_transactional; +import io.flamingock.store.mongodb.reactive.changes._002__insert_federico_happy_transactional; +import io.flamingock.store.mongodb.reactive.changes._003__insert_jorge_failed_non_transactional_rollback; +import io.flamingock.core.kit.audit.AuditTestSupport; +import io.flamingock.mongodb.reactive.kit.MongoDBReactiveTestKit; +import io.flamingock.core.kit.TestKit; +import io.flamingock.core.kit.audit.AuditTestHelper; +import io.flamingock.internal.core.operation.OperationException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.Collections; + +import static io.flamingock.core.kit.audit.AuditEntryExpectation.APPLIED; +import static io.flamingock.core.kit.audit.AuditEntryExpectation.FAILED; +import static io.flamingock.core.kit.audit.AuditEntryExpectation.ROLLED_BACK; +import static io.flamingock.core.kit.audit.AuditEntryExpectation.STARTED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * MongoDB-specific version of CoreStrategiesE2ETest demonstrating unified TestKit API + * with real MongoDB storage using Testcontainers. + * + *

This test suite replicates all scenarios from CoreStrategiesE2ETest but uses + * MongoDBReactiveTestKit with actual MongoDB persistence, proving the audit-store agnostic + * design works identically across storage implementations.

+ */ +@Testcontainers +class MongoDBReactiveCoreStrategiesE2ETest { + + @Container + static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.4.6")) + .withReuse(true); + + private MongoClient mongoClient; + private MongoDatabase database; + + @BeforeAll + static void beforeAll() { + mongoDBContainer.start(); + } + + @BeforeEach + void setUp() { + mongoClient = MongoClients.create(mongoDBContainer.getConnectionString()); + database = mongoClient.getDatabase("test"); + } + + @AfterEach + void tearDown() { + ReactiveMongoTestHelper.complete(database.drop()); // Clean between tests + mongoClient.close(); + } + + @Test + @DisplayName("Should execute non-transactional change using NonTx strategy with complete audit flow in MongoDB") + void testNonTransactionalChangeExecution() { + // Given - Create MongoDB test kit with real database storage + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test"); + TestKit testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem), mongoClient, database); + + // Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.emptyList()), + new CodeChangeTestDefinition(_002__insert_federico_happy_non_transactional.class, Collections.emptyList()) + ) + .WHEN(() -> testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .build() + .run()) + .THEN_VerifyAuditSequenceStrict( + STARTED("create-client-collection"), + APPLIED("create-client-collection"), + STARTED("insert-federico-document"), + APPLIED("insert-federico-document") + ) + .run(); + } + + @Test + @DisplayName("Should execute transactional change using SimpleTx/SharedTx strategy with complete audit flow in MongoDB") + void testTransactionalChangeExecution() { + // Given - Create MongoDB test kit with real database storage + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test"); + TestKit testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem), mongoClient, database); + AuditTestHelper auditHelper = testKit.getAuditHelper(); + + // Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.emptyList()), + new CodeChangeTestDefinition(_002__insert_federico_happy_transactional.class, Collections.emptyList()) + ) + .WHEN(() -> testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .build() + .run()) + .THEN_VerifyAuditSequenceStrict( + STARTED("create-client-collection"), + APPLIED("create-client-collection"), + STARTED("insert-federico-document"), + APPLIED("insert-federico-document") + ) + .run(); + } + + @Test + @DisplayName("Should execute multiple changes with correct audit sequence in MongoDB") + void testMultipleChangesExecution() { + // Given - Create MongoDB test kit with real database storage + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test"); + TestKit testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem), mongoClient, database); + AuditTestHelper auditHelper = testKit.getAuditHelper(); + + // Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.emptyList()), + new CodeChangeTestDefinition(_002__insert_federico_happy_non_transactional.class, Collections.emptyList()) + ) + .WHEN(() -> testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .build() + .run()) + .THEN_VerifyAuditSequenceStrict( + STARTED("create-client-collection"), + APPLIED("create-client-collection"), + STARTED("insert-federico-document"), + APPLIED("insert-federico-document") + ) + .run(); + } + + @Test + @DisplayName("Should handle failing transactional change with proper audit and rollback in MongoDB") + void testFailingTransactionalChangeWithRollback() { + // Given - Create MongoDB test kit with real database storage + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test"); + TestKit testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem), mongoClient, database); + AuditTestHelper auditHelper = testKit.getAuditHelper(); + + // Given-When-Then + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.emptyList()), + new CodeChangeTestDefinition(_003__insert_jorge_failed_non_transactional_rollback.class, Collections.emptyList()) + ) + .WHEN(() -> assertThrows(OperationException.class, () -> { + testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .build() + .run(); + })) + .THEN_VerifyAuditSequenceStrict( + STARTED("create-client-collection"), + APPLIED("create-client-collection"), + STARTED("insert-jorge-document"), + FAILED("insert-jorge-document"), + ROLLED_BACK("insert-jorge-document") + ) + .run(); + } + + @Test + @DisplayName("Should handle already-applied changes correctly on second run with MongoDB persistence") + void testAlreadyAppliedChangesSkipping() { + // Given - Create MongoDB test kit with persistent storage + MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test"); + TestKit testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem), mongoClient, database); + AuditTestHelper auditHelper = testKit.getAuditHelper(); + + // Given-When-Then - First execution + AuditTestSupport.withTestKit(testKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.emptyList()) + ) + .WHEN(() -> testKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .build() + .run()) + .THEN_VerifyAuditSequenceStrict( + STARTED("create-client-collection"), + APPLIED("create-client-collection") + ) + .run(); + + // Verify first execution with MongoDB storage + assertEquals(2, auditHelper.getAuditEntriesSorted().size()); + + // Second execution - create new kit using SAME MongoDB database to simulate persistence + TestKit secondRunKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem), mongoClient, database); + + AuditTestSupport.withTestKit(secondRunKit) + .GIVEN_Changes( + new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.emptyList()) + ) + .WHEN(() -> secondRunKit.createBuilder() + .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem)) + .addTargetSystem(mongoDBSyncTargetSystem) + .build() + .run()) + .THEN_VerifyAuditSequenceStrict( + STARTED("create-client-collection"), + APPLIED("create-client-collection") + ) + .run(); + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBTestHelper.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBTestHelper.java new file mode 100644 index 000000000..646a6e563 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBTestHelper.java @@ -0,0 +1,63 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive; + +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; +import io.flamingock.internal.util.TimeUtil; +import io.flamingock.internal.common.mongodb.MongoDBAuditMapper; +import org.bson.Document; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_CHANGE_ID; +import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_CREATED_AT; + +public class MongoDBTestHelper { + public final MongoDatabase mongoDatabase; + private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document())); + + public MongoDBTestHelper(MongoDatabase mongoDatabase) { + this.mongoDatabase = mongoDatabase; + } + + public boolean collectionExists(String collectionName) { + return ReactiveMongoTestHelper.collect(mongoDatabase.listCollectionNames()).contains(collectionName); + } + + public List getAuditLogSorted(String auditLogCollection) { + return ReactiveMongoTestHelper.collect(mongoDatabase.getCollection(auditLogCollection).find()) + .stream() + .sorted(Comparator.comparing(d -> TimeUtil.toLocalDateTime(d.get(KEY_CREATED_AT)))) + .map(document -> document.getString(KEY_CHANGE_ID)) + .collect(Collectors.toList()); + } + + public List getAuditEntriesSorted(String auditLogCollection) { + return ReactiveMongoTestHelper.collect(mongoDatabase.getCollection(auditLogCollection).find()) + .stream() + .map(MongoDBDocumentHelper::new) + .map(mapper::fromDocument) + .sorted() + .collect(Collectors.toList()); + } + + + +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/ReactiveMongoTestHelper.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/ReactiveMongoTestHelper.java new file mode 100644 index 000000000..a1a6dc0ae --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/ReactiveMongoTestHelper.java @@ -0,0 +1,83 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive; + +import io.flamingock.internal.common.core.error.FlamingockException; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +public final class ReactiveMongoTestHelper { + + private ReactiveMongoTestHelper() { + } + + public static T first(Publisher publisher) { + List values = collect(publisher); + return values.isEmpty() ? null : values.get(0); + } + + public static List collect(Publisher publisher) { + List values = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + publisher.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T value) { + values.add(value); + } + + @Override + public void onError(Throwable throwable) { + error.set(throwable); + latch.countDown(); + } + + @Override + public void onComplete() { + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new FlamingockException(e); + } + Throwable throwable = error.get(); + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } + if (throwable != null) { + throw new FlamingockException(throwable); + } + return values; + } + + public static void complete(Publisher publisher) { + collect(publisher); + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_000__mongock_create_authors_collection.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_000__mongock_create_authors_collection.java new file mode 100644 index 000000000..5c00cda9f --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_000__mongock_create_authors_collection.java @@ -0,0 +1,79 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes; + +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Rollback; +import io.mongock.api.annotations.ChangeUnit; +import io.mongock.api.annotations.Execution; +import io.mongock.api.annotations.RollbackExecution; +import io.mongock.api.annotations.BeforeExecution; +import io.mongock.api.annotations.RollbackBeforeExecution; +import org.bson.Document; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete; + + +@TargetSystem(id = "mongodb") +@Change(id = "create-author-collection", author = "mongock") +@ChangeUnit(id = "create-author-collection", order = "000", author = "mongock") +public class _000__mongock_create_authors_collection { + + public final static int INITIAL_CLIENTS = 10; + public final static String CLIENTS_COLLECTION_NAME = "mongockClientCollection"; + + @BeforeExecution + public void beforeExecution(MongoDatabase mongoDatabase) { + + complete(mongoDatabase.createCollection(CLIENTS_COLLECTION_NAME)); + } + + @RollbackBeforeExecution + public void rollbackBeforeExecution(MongoDatabase mongoDatabase) { + + complete(mongoDatabase.getCollection(CLIENTS_COLLECTION_NAME).drop()); + } + + @Apply + @Execution + public void apply(MongoDatabase mongoDatabase) { + + complete(mongoDatabase.getCollection(CLIENTS_COLLECTION_NAME) + .insertMany(IntStream.range(0, INITIAL_CLIENTS) + .mapToObj(_000__mongock_create_authors_collection::getClient) + .collect(Collectors.toList()))); + } + + @Rollback + @RollbackExecution + public void rollback(MongoDatabase mongoDatabase) { + complete(mongoDatabase.getCollection(CLIENTS_COLLECTION_NAME).deleteMany(new Document())); + } + + private static Document getClient(int i) { + return new Document() + .append("name", "name-" + i) + .append("email","email-" + i) + .append("phone","phone" + i). + append("country","country" + i); + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_001__create_client_collection_happy.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_001__create_client_collection_happy.java new file mode 100644 index 000000000..69d64af01 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_001__create_client_collection_happy.java @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes; + +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; + +import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete; + +@Change( id="create-client-collection", transactional = false, author = "aperezdieppa") +@TargetSystem(id = "mongodb") +public class _001__create_client_collection_happy { + + @Apply + public void apply(MongoDatabase mongoDatabase) { + complete(mongoDatabase.createCollection("clientCollection")); + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_002__insert_federico_happy_non_transactional.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_002__insert_federico_happy_non_transactional.java new file mode 100644 index 000000000..b3b5b76d5 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_002__insert_federico_happy_non_transactional.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes; + +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; +import org.bson.Document; + +import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete; + +@TargetSystem(id = "mongodb") +@Change( id="insert-federico-document", author = "aperezdieppa") +public class _002__insert_federico_happy_non_transactional { + + @Apply + public void apply(MongoDatabase mongoDatabase) { + MongoCollection collection = mongoDatabase.getCollection("clientCollection"); + complete(collection.insertOne(new Document().append("name", "Federico"))); + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_002__insert_federico_happy_transactional.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_002__insert_federico_happy_transactional.java new file mode 100644 index 000000000..84c7c76d0 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_002__insert_federico_happy_transactional.java @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes; + +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; +import org.bson.Document; + +import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete; + +@Change( id="insert-federico-document" , author = "aperezdieppa") +@TargetSystem(id = "mongodb") +public class _002__insert_federico_happy_transactional { + + @Apply + public void apply(MongoDatabase mongoDatabase, ClientSession clientSession) { + MongoCollection collection = mongoDatabase.getCollection("clientCollection"); + complete(collection.insertOne(clientSession, new Document().append("name", "Federico"))); + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_non_transactional_non_rollback.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_non_transactional_non_rollback.java new file mode 100644 index 000000000..791d55d6a --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_non_transactional_non_rollback.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes; + +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; +import org.bson.Document; + +import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete; + +@TargetSystem(id = "mongodb") +@Change( id="insert-jorge-document" , transactional = false, author = "aperezdieppa") +public class _003__insert_jorge_failed_non_transactional_non_rollback { + + @Apply + public void apply(MongoDatabase mongoDatabase) { + MongoCollection collection = mongoDatabase.getCollection("clientCollection"); + complete(collection.insertOne(new Document().append("name", "Jorge"))); + throw new RuntimeException("test"); + } + +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_non_transactional_rollback.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_non_transactional_rollback.java new file mode 100644 index 000000000..d0831f925 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_non_transactional_rollback.java @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes; + +import org.bson.Document; + +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; +import io.flamingock.api.annotations.Rollback; + +import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete; + +@TargetSystem(id = "mongodb") +@Change( id="insert-jorge-document" , author = "aperezdieppa") +public class _003__insert_jorge_failed_non_transactional_rollback { + + @Apply + public void apply(MongoDatabase mongoDatabase) { + MongoCollection collection = mongoDatabase.getCollection("clientCollection"); + complete(collection.insertOne(new Document().append("name", "Jorge"))); + throw new RuntimeException("test"); + } + + @Rollback + public void rollback(MongoDatabase mongoDatabase) { + MongoCollection collection = mongoDatabase.getCollection("clientCollection"); + complete(collection.deleteOne(new Document().append("name", "Jorge"))); + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_transactional_non_rollback.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_transactional_non_rollback.java new file mode 100644 index 000000000..2f19cc354 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_transactional_non_rollback.java @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes; + +import org.bson.Document; + +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; + +import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete; + +@TargetSystem(id = "mongodb") +@Change( id="insert-jorge-document", author = "aperezdieppa") +public class _003__insert_jorge_failed_transactional_non_rollback { + + @Apply + public void apply(MongoDatabase mongoDatabase, ClientSession clientSession) { + MongoCollection collection = mongoDatabase.getCollection("clientCollection"); + complete(collection.insertOne(clientSession, new Document().append("name", "Jorge"))); + throw new RuntimeException("test"); + } + +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_happy_non_transactional.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_happy_non_transactional.java new file mode 100644 index 000000000..8791f0566 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_happy_non_transactional.java @@ -0,0 +1,36 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes; + +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; +import org.bson.Document; + +import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete; + +@TargetSystem(id = "mongodb") +@Change( id="insert-jorge-document" , author = "aperezdieppa") +public class _003__insert_jorge_happy_non_transactional { + + @Apply + public void apply(MongoDatabase mongoDatabase) { + MongoCollection collection = mongoDatabase.getCollection("clientCollection"); + complete(collection.insertOne(new Document().append("name", "Jorge"))); + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_happy_transactional.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_happy_transactional.java new file mode 100644 index 000000000..893af04f0 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_happy_transactional.java @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes; + +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; +import org.bson.Document; + +import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete; + +@TargetSystem(id = "mongodb") +@Change( id="insert-jorge-document", author = "aperezdieppa") +public class _003__insert_jorge_happy_transactional { + + @Apply + public void apply(MongoDatabase mongoDatabase, ClientSession clientSession) { + MongoCollection collection = mongoDatabase.getCollection("clientCollection"); + complete(collection.insertOne(clientSession, new Document().append("name", "Jorge"))); + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_001__NonTxTransactionalFalseChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_001__NonTxTransactionalFalseChange.java new file mode 100644 index 000000000..55deae024 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_001__NonTxTransactionalFalseChange.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes.audit; + +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; +/** + * Change that produces NON_TX txStrategy via transactional=false annotation. + * Used for testing audit persistence of NON_TX transaction type. + */ +@TargetSystem(id = "mongodb") +@Change(id = "non-tx-transactional-false", transactional = false, author = "aperezdieppa") +public class _001__NonTxTransactionalFalseChange { + + @Apply + public void apply() { + // Simple operation that completes successfully + System.out.println("Executing NON_TX change via transactional=false"); + } +} \ No newline at end of file diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_002__TxSharedDefaultChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_002__TxSharedDefaultChange.java new file mode 100644 index 000000000..e0a1b0330 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_002__TxSharedDefaultChange.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes.audit; + +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; +/** + * Change that produces TX_SHARED txStrategy via default behavior. + * Used for testing audit persistence of TX_SHARED transaction type. + */ +@TargetSystem(id = "mongodb") +@Change(id = "tx-shared-default", transactional = true, author = "aperezdieppa") +public class _002__TxSharedDefaultChange { + + @Apply + public void apply() { + // Simple operation that completes successfully + System.out.println("Executing TX_SHARED change via default behavior"); + } +} \ No newline at end of file diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_003__NonTxTargetSystemChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_003__NonTxTargetSystemChange.java new file mode 100644 index 000000000..514b88c28 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_003__NonTxTargetSystemChange.java @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes.audit; + +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; + +/** + * Change that produces NON_TX txStrategy via non-transactional DefaultTargetSystem. + * Used for testing audit persistence of NON_TX transaction type with custom target system. + */ +@TargetSystem(id = "non-tx-system") +@Change(id = "non-tx-target-system", author = "aperezdieppa") +public class _003__NonTxTargetSystemChange { + + @Apply + public void apply() { + // Simple operation that completes successfully + System.out.println("Executing NON_TX change via DefaultTargetSystem"); + } +} \ No newline at end of file diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_004__TxSeparateAndSameMongoClientChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_004__TxSeparateAndSameMongoClientChange.java new file mode 100644 index 000000000..dd9246b13 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_004__TxSeparateAndSameMongoClientChange.java @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes.audit; + +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; + +/** + * Change that produces TX_SHARED txStrategy via explicit MongoDBReactiveTargetSystem with same MongoClient. + * Used for testing audit persistence of TX_SHARED transaction type with explicit target system. + */ +@TargetSystem(id = "mongo-system") +@Change(id = "tx-separate-no-marker", transactional = true, author = "aperezdieppa") +public class _004__TxSeparateAndSameMongoClientChange { + + @Apply + public void apply() { + // Simple operation that completes successfully + System.out.println("Executing TX_SHARED change via explicit MongoDBReactiveTargetSystem with same MongoClient"); + } +} \ No newline at end of file diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_005__TxSeparateChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_005__TxSeparateChange.java new file mode 100644 index 000000000..666d3cb86 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_005__TxSeparateChange.java @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes.audit; + +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; + +/** + * Change that produces TX_SEPARATE_NO_MARKER txStrategy via MongoDBReactiveTargetSystem with different MongoClient. + * Used for testing audit persistence of TX_SEPARATE_NO_MARKER transaction type. + */ +@TargetSystem(id = "tx-separate-system") +@Change(id = "tx-separate-no-marker", transactional = true, author = "aperezdieppa") +public class _005__TxSeparateChange { + + @Apply + public void apply() { + // Simple operation that completes successfully + System.out.println("Executing TX_SEPARATE_NO_MARKER change via MongoDBReactiveTargetSystem with different MongoClient"); + } +} \ No newline at end of file diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_006__FailingChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_006__FailingChange.java new file mode 100644 index 000000000..530fdf8ba --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_006__FailingChange.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes.audit; + +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; +/** + * Change that intentionally fails to test error audit scenarios. + * Used for testing audit persistence of error fields like errorTrace. + */ +@TargetSystem(id = "mongodb") +@Change(id = "failing-change", transactional = false, author = "aperezdieppa") +public class _006__FailingChange { + + @Apply + public void apply() { + // Intentionally throw exception to test error audit + throw new RuntimeException("Intentional failure for audit testing"); + } +} \ No newline at end of file diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_007__MetadataChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_007__MetadataChange.java new file mode 100644 index 000000000..481cb6f58 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_007__MetadataChange.java @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.changes.audit; + +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.TargetSystem; +/** + * Change for testing metadata serialization in audit entries. + * Used for testing audit persistence of metadata field. + */ +@TargetSystem(id = "mongodb") +@Change(id = "metadata-change", transactional = false, author = "aperezdieppa") +public class _007__MetadataChange { + + @Apply + public void apply() { + // Simple operation that completes successfully + // Metadata testing will be done through test configuration + System.out.println("Executing change with metadata for audit testing"); + } +} \ No newline at end of file diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditPersistenceTest.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditPersistenceTest.java new file mode 100644 index 000000000..da5dd7cd7 --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditPersistenceTest.java @@ -0,0 +1,133 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.internal; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.RecoveryStrategy; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.core.audit.AuditTxType; +import io.flamingock.internal.core.configuration.community.CommunityConfigurable; +import io.flamingock.internal.util.id.RunnerId; +import io.flamingock.reactive.util.PublisherSync; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +@Testcontainers +class MongoDBReactiveAuditPersistenceTest { + + private static final String DB_NAME = "test"; + private static final String AUDIT_COLLECTION = "testFlamingockAudit"; + + @Container + static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + private MongoClient mongoClient; + private MongoDatabase database; + private MongoDBReactiveAuditPersistence persistence; + + @BeforeEach + void beforeEach() { + mongoClient = MongoClients.create(mongoDBContainer.getConnectionString()); + database = mongoClient.getDatabase(DB_NAME); + persistence = new MongoDBReactiveAuditPersistence( + mock(CommunityConfigurable.class), + database, + AUDIT_COLLECTION, + ReadConcern.MAJORITY, + ReadPreference.primary(), + WriteConcern.MAJORITY.withJournal(true), + true); + persistence.initialize(RunnerId.fromString("runner-1")); + } + + @AfterEach + void afterEach() { + PublisherSync.complete(database.drop()); + mongoClient.close(); + } + + @Test + @DisplayName("Should write and read audit entries") + void shouldWriteAndReadAuditEntries() { + AuditEntry started = auditEntry("change-1", AuditEntry.Status.STARTED); + AuditEntry applied = auditEntry("change-1", AuditEntry.Status.APPLIED); + + persistence.writeEntry(started); + persistence.writeEntry(applied); + + List history = persistence.getAuditHistory(); + List states = history.stream() + .map(AuditEntry::getState) + .collect(Collectors.toList()); + + assertEquals(2, history.size()); + assertTrue(states.contains(AuditEntry.Status.STARTED)); + assertTrue(states.contains(AuditEntry.Status.APPLIED)); + assertTrue(history.stream().allMatch(entry -> "change-1".equals(entry.getChangeId()))); + } + + @Test + @DisplayName("Should replace an existing audit entry with the same execution, change and state") + void shouldReplaceExistingAuditEntry() { + persistence.writeEntry(auditEntry("change-1", AuditEntry.Status.STARTED)); + persistence.writeEntry(auditEntry("change-1", AuditEntry.Status.STARTED)); + + assertEquals(1, persistence.getAuditHistory().size()); + } + + private AuditEntry auditEntry(String changeId, AuditEntry.Status status) { + return new AuditEntry( + "execution-1", + "stage-1", + changeId, + "test-author", + LocalDateTime.now(), + status, + AuditEntry.ChangeType.STANDARD_CODE, + "TestChange", + "apply", + "TestChange.java", + 10L, + "localhost", + null, + false, + null, + AuditTxType.NON_TX, + "mongodb", + "001", + RecoveryStrategy.MANUAL_INTERVENTION, + false); + } +} diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveLockServiceTest.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveLockServiceTest.java new file mode 100644 index 000000000..5aabcd4bd --- /dev/null +++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveLockServiceTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.store.mongodb.reactive.internal; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.internal.core.external.store.lock.LockAcquisition; +import io.flamingock.internal.core.external.store.lock.LockKey; +import io.flamingock.internal.core.external.store.lock.LockServiceException; +import io.flamingock.internal.util.TimeService; +import io.flamingock.internal.util.id.RunnerId; +import io.flamingock.reactive.util.PublisherSync; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Testcontainers +class MongoDBReactiveLockServiceTest { + + private static final String DB_NAME = "test"; + private static final String LOCK_COLLECTION = "testFlamingockLock"; + private static final LockKey LOCK_KEY = LockKey.fromString("lockKey1"); + + @Container + static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + private MongoClient mongoClient; + private MongoDatabase database; + private MongoDBReactiveLockService lockService; + + @BeforeEach + void beforeEach() { + mongoClient = MongoClients.create(mongoDBContainer.getConnectionString()); + database = mongoClient.getDatabase(DB_NAME); + lockService = new MongoDBReactiveLockService( + database, + LOCK_COLLECTION, + ReadConcern.MAJORITY, + ReadPreference.primary(), + WriteConcern.MAJORITY.withJournal(true), + TimeService.getDefault()); + lockService.initialize(true); + } + + @AfterEach + void afterEach() { + PublisherSync.complete(database.drop()); + mongoClient.close(); + } + + @Test + @DisplayName("Should acquire and read lock") + void shouldAcquireAndReadLock() { + RunnerId runnerId = RunnerId.fromString("runner-1"); + + LockAcquisition lockAcquisition = lockService.upsert(LOCK_KEY, runnerId, 10000); + LockAcquisition lockInfo = lockService.getLockInfo(LOCK_KEY); + + assertEquals(runnerId, lockAcquisition.getOwner()); + assertEquals(10000, lockAcquisition.getAcquiredForMillis()); + assertEquals(runnerId, lockInfo.getOwner()); + } + + @Test + @DisplayName("Should not acquire a held lock with a different owner") + void shouldNotAcquireHeldLockWithDifferentOwner() { + lockService.upsert(LOCK_KEY, RunnerId.fromString("runner-1"), 10000); + + assertThrows(LockServiceException.class, () -> + lockService.upsert(LOCK_KEY, RunnerId.fromString("runner-2"), 10000)); + } + + @Test + @DisplayName("Should extend and release lock") + void shouldExtendAndReleaseLock() { + RunnerId runnerId = RunnerId.fromString("runner-1"); + lockService.upsert(LOCK_KEY, runnerId, 10000); + + LockAcquisition extension = lockService.extendLock(LOCK_KEY, runnerId, 20000); + lockService.releaseLock(LOCK_KEY, runnerId); + + assertEquals(runnerId, extension.getOwner()); + assertEquals(20000, extension.getAcquiredForMillis()); + assertNull(lockService.getLockInfo(LOCK_KEY)); + } +} diff --git a/community/flamingock-mongodb-sync-auditstore/build.gradle.kts b/community/flamingock-mongodb-sync-auditstore/build.gradle.kts index ce322596c..df389916e 100644 --- a/community/flamingock-mongodb-sync-auditstore/build.gradle.kts +++ b/community/flamingock-mongodb-sync-auditstore/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.utils.extendsFrom dependencies { implementation(project(":utils:mongodb-util")) + implementation(project(":utils:mongodb-sync-util")) implementation(project(":core:flamingock-core")) api(project(":core:target-systems:flamingock-mongodb-externalsystem-api")) @@ -34,4 +35,4 @@ java { configurations.testImplementation { extendsFrom(configurations.compileOnly.get()) -} \ No newline at end of file +} diff --git a/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncAuditor.java b/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncAuditor.java index f9fa329e0..8d5e6a0cf 100644 --- a/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncAuditor.java +++ b/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncAuditor.java @@ -28,7 +28,7 @@ import io.flamingock.internal.common.mongodb.CollectionInitializator; import io.flamingock.internal.common.mongodb.MongoDBAuditMapper; import io.flamingock.internal.common.mongodb.MongoDBSyncCollectionHelper; -import io.flamingock.internal.common.mongodb.MongoDBSyncDocumentHelper; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; import io.flamingock.internal.core.external.store.audit.LifecycleAuditWriter; import io.flamingock.internal.util.Result; import io.flamingock.internal.util.log.FlamingockLoggerFactory; @@ -49,7 +49,7 @@ public class MongoDBSyncAuditor implements LifecycleAuditWriter, AuditReader { private static final Logger logger = FlamingockLoggerFactory.getLogger("MongoDBSyncAuditor"); private final MongoCollection collection; - private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBSyncDocumentHelper(new Document())); + private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document())); MongoDBSyncAuditor(MongoDatabase database, String collectionName, @@ -63,9 +63,9 @@ public class MongoDBSyncAuditor implements LifecycleAuditWriter, AuditReader { } protected void initialize(boolean autoCreate) { - CollectionInitializator initializer = new CollectionInitializator<>( + CollectionInitializator initializer = new CollectionInitializator<>( new MongoDBSyncCollectionHelper(collection), - () -> new MongoDBSyncDocumentHelper(new Document()), + () -> new MongoDBDocumentHelper(new Document()), new String[]{KEY_EXECUTION_ID, KEY_CHANGE_ID, KEY_STATE} ); if (autoCreate) { @@ -99,7 +99,7 @@ public List getAuditHistory() { return collection.find() .into(new LinkedList<>()) .stream() - .map(MongoDBSyncDocumentHelper::new) + .map(MongoDBDocumentHelper::new) .map(mapper::fromDocument) .collect(Collectors.toList()); } diff --git a/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncLockService.java b/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncLockService.java index fba382a3d..6387d8b44 100644 --- a/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncLockService.java +++ b/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncLockService.java @@ -27,7 +27,7 @@ import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.result.UpdateResult; import io.flamingock.internal.common.mongodb.MongoDBSyncCollectionHelper; -import io.flamingock.internal.common.mongodb.MongoDBSyncDocumentHelper; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; import io.flamingock.internal.util.id.RunnerId; import io.flamingock.internal.util.TimeService; import io.flamingock.internal.core.external.store.lock.community.CommunityLockService; @@ -50,7 +50,7 @@ public class MongoDBSyncLockService implements CommunityLockService { - private final MongoDBLockMapper mapper = new MongoDBLockMapper<>(() -> new MongoDBSyncDocumentHelper(new Document())); + private final MongoDBLockMapper mapper = new MongoDBLockMapper<>(() -> new MongoDBDocumentHelper(new Document())); private final MongoCollection collection; @@ -82,9 +82,9 @@ protected MongoDBSyncLockService(MongoCollection collection, } public void initialize(boolean autoCreate) { - CollectionInitializator initializer = new CollectionInitializator<>( + CollectionInitializator initializer = new CollectionInitializator<>( new MongoDBSyncCollectionHelper(collection), - () -> new MongoDBSyncDocumentHelper(new Document()), + () -> new MongoDBDocumentHelper(new Document()), new String[]{KEY_FIELD} ); if (autoCreate) { @@ -113,7 +113,7 @@ public LockAcquisition extendLock(LockKey key, RunnerId owner, long leaseMillis) public LockAcquisition getLockInfo(LockKey lockKey) { Document result = collection.find(new Document().append(KEY_FIELD, lockKey.toString())).first(); if (result != null) { - return mapper.fromDocument(new MongoDBSyncDocumentHelper(result)); + return mapper.fromDocument(new MongoDBDocumentHelper(result)); } return null; } diff --git a/community/flamingock-mongodb-sync-auditstore/src/test/java/io/flamingock/store/mongodb/sync/MongoDBTestHelper.java b/community/flamingock-mongodb-sync-auditstore/src/test/java/io/flamingock/store/mongodb/sync/MongoDBTestHelper.java index 90e89f2ae..1c1d02869 100644 --- a/community/flamingock-mongodb-sync-auditstore/src/test/java/io/flamingock/store/mongodb/sync/MongoDBTestHelper.java +++ b/community/flamingock-mongodb-sync-auditstore/src/test/java/io/flamingock/store/mongodb/sync/MongoDBTestHelper.java @@ -17,7 +17,7 @@ import com.mongodb.client.MongoDatabase; import io.flamingock.internal.common.core.audit.AuditEntry; -import io.flamingock.internal.common.mongodb.MongoDBSyncDocumentHelper; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; import io.flamingock.internal.util.TimeUtil; import io.flamingock.internal.common.mongodb.MongoDBAuditMapper; import org.bson.Document; @@ -33,7 +33,7 @@ public class MongoDBTestHelper { public final MongoDatabase mongoDatabase; - private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBSyncDocumentHelper(new Document())); + private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document())); public MongoDBTestHelper(MongoDatabase mongoDatabase) { this.mongoDatabase = mongoDatabase; @@ -57,7 +57,7 @@ public List getAuditEntriesSorted(String auditLogCollection) { return mongoDatabase.getCollection(auditLogCollection).find() .into(new LinkedList<>()) .stream() - .map(MongoDBSyncDocumentHelper::new) + .map(MongoDBDocumentHelper::new) .map(mapper::fromDocument) .sorted() .collect(Collectors.toList()); diff --git a/core/flamingock-bom/build.gradle.kts b/core/flamingock-bom/build.gradle.kts index d3b8f9567..334a3551e 100644 --- a/core/flamingock-bom/build.gradle.kts +++ b/core/flamingock-bom/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { constraints { // Flamingock Core api("io.flamingock:flamingock-general-util:${generalUtilVersion}") + api("io.flamingock:flamingock-reactive-util:${version}") api("io.flamingock:flamingock-core-api:${coreApiVersion}") api("io.flamingock:flamingock-template-api:${templateApiVersion}") api("io.flamingock:flamingock-test-support:${version}") @@ -23,6 +24,7 @@ dependencies { // Flamingock Community api("io.flamingock:flamingock-community:${version}") + api("io.flamingock:flamingock-mongodb-reactive-auditstore:$version") api("io.flamingock:flamingock-mongodb-sync-auditstore:$version") api("io.flamingock:flamingock-mongodb-springdata-auditstore:${version}") api("io.flamingock:flamingock-couchbase-auditstore:$version") @@ -46,6 +48,9 @@ dependencies { // Mongodb api("io.flamingock:flamingock-mongodb-externalsystem-api:${version}") + api("io.flamingock:flamingock-mongodb-reactive-externalsystem-api:${version}") + api("io.flamingock:flamingock-mongodb-reactive-targetsystem:${version}") + api("io.flamingock:flamingock-mongodb-springdata-reactive-targetsystem:${version}") api("io.flamingock:flamingock-mongodb-sync-targetsystem:${version}") api("io.flamingock:flamingock-mongodb-springdata-targetsystem:${version}") api("io.flamingock:flamingock-mongodb-sync-template:${mongodbTemplateVersion}") diff --git a/core/target-systems/flamingock-mongodb-reactive-externalsystem-api/build.gradle.kts b/core/target-systems/flamingock-mongodb-reactive-externalsystem-api/build.gradle.kts new file mode 100644 index 000000000..63012fb52 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-externalsystem-api/build.gradle.kts @@ -0,0 +1,15 @@ +val coreApiVersion: String by extra + +dependencies { + implementation("io.flamingock:flamingock-core-api:${coreApiVersion}") + + compileOnly("org.mongodb:mongodb-driver-reactivestreams:4.0.0") +} + +description = "MongoDB reactive external system api" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} diff --git a/core/target-systems/flamingock-mongodb-reactive-externalsystem-api/src/main/java/io/flamingock/externalsystem/mongodb/reactive/api/MongoDBReactiveExternalSystem.java b/core/target-systems/flamingock-mongodb-reactive-externalsystem-api/src/main/java/io/flamingock/externalsystem/mongodb/reactive/api/MongoDBReactiveExternalSystem.java new file mode 100644 index 000000000..c2ba24770 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-externalsystem-api/src/main/java/io/flamingock/externalsystem/mongodb/reactive/api/MongoDBReactiveExternalSystem.java @@ -0,0 +1,24 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.externalsystem.mongodb.reactive.api; + +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.external.ExternalSystem; + +public interface MongoDBReactiveExternalSystem extends ExternalSystem { + + MongoDatabase getMongoDatabase(); +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/build.gradle.kts b/core/target-systems/flamingock-mongodb-reactive-targetsystem/build.gradle.kts new file mode 100644 index 000000000..ad3dbb813 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/build.gradle.kts @@ -0,0 +1,26 @@ +dependencies { + api(project(":core:flamingock-core")) + implementation(project(":utils:mongodb-util")) + implementation(project(":utils:mongodb-reactive-util")) + api(project(":utils:flamingock-reactive-util")) + api(project(":core:target-systems:flamingock-mongodb-reactive-externalsystem-api")) + + compileOnly("org.mongodb:mongodb-driver-reactivestreams:4.0.0") + + testImplementation("org.testcontainers:testcontainers-mongodb:2.0.2") + testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.2") + testImplementation(project(":cloud:flamingock-cloud")) + testImplementation(project(":utils:test-util")) +} + +description = "MongoDB reactive streams driver target system for database change operations" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} + +configurations.testImplementation { + extendsFrom(configurations.compileOnly.get()) +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveAuditMarker.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveAuditMarker.java new file mode 100644 index 000000000..f9daca643 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveAuditMarker.java @@ -0,0 +1,153 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType; +import io.flamingock.internal.common.mongodb.CollectionInitializator; +import io.flamingock.internal.common.mongodb.MongoDBReactiveCollectionHelper; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; +import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark; +import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMarker; +import io.flamingock.internal.core.transaction.TransactionManager; +import io.flamingock.internal.util.constants.CommunityPersistenceConstants; +import io.flamingock.reactive.util.PublisherSync; +import org.bson.Document; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + + +public class MongoDBReactiveAuditMarker implements TargetSystemAuditMarker { + public static final String OPERATION = "operation"; + private static final String CHANGE_ID = "changeId"; + private final MongoCollection auditMarkerCollection; + private final TransactionManager txManager; + + public MongoDBReactiveAuditMarker(MongoCollection auditMarkerCollection, + TransactionManager txManager) { + this.auditMarkerCollection = auditMarkerCollection; + this.txManager = txManager; + } + + public static Builder builder(MongoDatabase mongoDatabase, TransactionManager txManager) { + return new Builder(mongoDatabase, txManager); + } + + public static TargetSystemAuditMark mapToOnGoingStatus(Document document) { + TargetSystemAuditMarkType operation = TargetSystemAuditMarkType.valueOf(document.getString(OPERATION)); + return new TargetSystemAuditMark(document.getString(CHANGE_ID), operation); + } + + @Override + public Set listAll() { + return PublisherSync.collect(auditMarkerCollection.find()) + .stream() + .map(MongoDBReactiveAuditMarker::mapToOnGoingStatus) + .collect(Collectors.toCollection(HashSet::new)); + } + + @Override + public void clearMark(String changeId) { + PublisherSync.first(auditMarkerCollection.deleteMany(Filters.eq(CHANGE_ID, changeId))); + } + + @Override + public void mark(TargetSystemAuditMark auditMark) { + + Document filter = new Document(CHANGE_ID, auditMark.getChangeId()); + + // Define the new document to replace or insert + Document newDocument = new Document(CHANGE_ID, auditMark.getChangeId()) + .append(OPERATION, auditMark.getOperation().name()); + + ClientSession clientSession = txManager.getSessionOrThrow(auditMark.getChangeId()); + PublisherSync.first(auditMarkerCollection.updateOne( + clientSession, + filter, + new Document("$set", newDocument), + new UpdateOptions().upsert(true))); + } + + public static class Builder { + + private final MongoDatabase mongoDatabase; + private final TransactionManager txManager; + private boolean autoCreate = true; + private String collectionName = CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME; + private ReadConcern readConcern; + private ReadPreference readPreference; + private WriteConcern writeConcern; + + public Builder(MongoDatabase mongoDatabase, TransactionManager txManager) { + this.mongoDatabase = mongoDatabase; + this.txManager = txManager; + } + + public Builder setCollectionName(String collectionName) { + this.collectionName = collectionName; + return this; + } + + public Builder withAutoCreate(boolean autoCreate) { + this.autoCreate = autoCreate; + return this; + } + + public Builder withReadConcern(ReadConcern readConcern) { + this.readConcern = readConcern; + return this; + } + + public Builder withReadPreference(ReadPreference readPreference) { + this.readPreference = readPreference; + return this; + } + + public Builder withWriteConcern(WriteConcern writeConcern) { + this.writeConcern = writeConcern; + return this; + } + + public MongoDBReactiveAuditMarker build() { + MongoCollection collection = mongoDatabase.getCollection(collectionName) +// .withReadConcern(readConcern) +// .withReadPreference(readPreference) +// .withWriteConcern(writeConcern) + ; + + CollectionInitializator initializer = new CollectionInitializator<>( + new MongoDBReactiveCollectionHelper(collection), + () -> new MongoDBDocumentHelper(new Document()), + new String[]{CHANGE_ID} + ); + if (autoCreate) { + initializer.initialize(); + } else { + initializer.justValidateCollection(); + } + return new MongoDBReactiveAuditMarker(collection, txManager); + } + } +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystem.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystem.java new file mode 100644 index 000000000..6253a34fa --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystem.java @@ -0,0 +1,155 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.externalsystem.mongodb.reactive.api.MongoDBReactiveExternalSystem; +import io.flamingock.internal.common.core.audit.AuditHistoryReader; +import io.flamingock.internal.common.core.audit.AuditReaderType; +import io.flamingock.internal.common.core.context.ContextResolver; +import io.flamingock.internal.common.core.error.FlamingockException; +import io.flamingock.internal.common.core.transaction.TransactionWrapper; +import io.flamingock.internal.core.builder.FlamingockEdition; +import io.flamingock.internal.core.external.targets.TransactionalTargetSystem; +import io.flamingock.internal.core.external.targets.mark.NoOpTargetSystemAuditMarker; +import io.flamingock.internal.core.transaction.TransactionManager; +import io.flamingock.reactive.util.PublisherSync; + +import java.util.Optional; + +import static io.flamingock.internal.core.builder.FlamingockEdition.COMMUNITY; + +public class MongoDBReactiveTargetSystem extends TransactionalTargetSystem + implements MongoDBReactiveExternalSystem { + + private final MongoClient mongoClient; + private final String databaseName; + private MongoDatabase database; + private WriteConcern writeConcern = WriteConcern.MAJORITY.withJournal(true); + private ReadConcern readConcern = ReadConcern.MAJORITY; + private ReadPreference readPreference = ReadPreference.primary(); + private MongoDBReactiveTxWrapper txWrapper; + + public MongoDBReactiveTargetSystem(String id, MongoClient mongoClient, String databaseName) { + super(id); + this.mongoClient = mongoClient; + this.databaseName = databaseName; + } + + public MongoDBReactiveTargetSystem withReadConcern(ReadConcern readConcern) { + this.readConcern = readConcern; + return this; + } + + public MongoDBReactiveTargetSystem withReadPreference(ReadPreference readPreference) { + this.readPreference = readPreference; + return this; + } + + public MongoDBReactiveTargetSystem withWriteConcern(WriteConcern writeConcern) { + this.writeConcern = writeConcern; + return this; + } + + public MongoClient getClient() { + return mongoClient; + } + + @Override + public MongoDatabase getMongoDatabase() { + if (mongoClient == null || databaseName == null || databaseName.isEmpty()) { + throw new FlamingockException("TargetSystem is not initialized. The 'mongoClient' instance and 'databaseName' property are required."); + } + return mongoClient.getDatabase(databaseName); + } + + public String getDatabaseName() { + return databaseName; + } + + public WriteConcern getWriteConcern() { + return writeConcern; + } + + public ReadConcern getReadConcern() { + return readConcern; + } + + public ReadPreference getReadPreference() { + return readPreference; + } + + public TransactionManager getTxManager() { + return txWrapper.getTxManager(); + } + + @Override + public void initialize(ContextResolver baseContext) { + this.validate(); + targetSystemContext.addDependency(mongoClient); + database = mongoClient.getDatabase(databaseName) + .withReadConcern(readConcern) + .withReadPreference(readPreference) + .withWriteConcern(writeConcern); + targetSystemContext.addDependency(database); + + TransactionManager txManager = + new TransactionManager<>(() -> PublisherSync.first(mongoClient.startSession())); + txWrapper = new MongoDBReactiveTxWrapper(txManager); + FlamingockEdition edition = baseContext.getDependencyValue(FlamingockEdition.class).orElse(COMMUNITY); + auditMarker = edition == COMMUNITY + ? new NoOpTargetSystemAuditMarker(this.getId()) + : MongoDBReactiveAuditMarker.builder(database, txManager).build(); + } + + private void validate() { + if (mongoClient == null) { + throw new FlamingockException("The 'mongoClient' instance is required."); + } + if (databaseName == null || databaseName.trim().isEmpty()) { + throw new FlamingockException("The 'databaseName' property is required."); + } + if (readConcern == null) { + throw new FlamingockException("The 'readConcern' property is required."); + } + if (readPreference == null) { + throw new FlamingockException("The 'readPreference' property is required."); + } + if (writeConcern == null) { + throw new FlamingockException("The 'writeConcern' property is required."); + } + } + + @Override + protected MongoDBReactiveTargetSystem getSelf() { + return this; + } + + @Override + public TransactionWrapper getTxWrapper() { + return txWrapper; + } + + @Override + public Optional getAuditAuditReader(AuditReaderType type) { + return Optional.empty(); + } +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTxWrapper.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTxWrapper.java new file mode 100644 index 000000000..8b929d02a --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTxWrapper.java @@ -0,0 +1,126 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive; + +import com.mongodb.TransactionOptions; +import com.mongodb.reactivestreams.client.ClientSession; +import io.flamingock.internal.common.core.context.Dependency; +import io.flamingock.internal.common.core.context.RuntimeContext; +import io.flamingock.internal.common.core.error.DatabaseTransactionException; +import io.flamingock.internal.common.core.transaction.TransactionWrapper; +import io.flamingock.internal.core.change.navigation.step.FailedStep; +import io.flamingock.internal.core.transaction.TransactionManager; +import io.flamingock.internal.util.log.FlamingockLoggerFactory; +import io.flamingock.reactive.util.PublisherSync; +import org.slf4j.Logger; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.function.Function; + +public class MongoDBReactiveTxWrapper implements TransactionWrapper { + + private static final Logger logger = FlamingockLoggerFactory.getLogger("MongoReactiveTx"); + private final TransactionManager sessionManager; + + public MongoDBReactiveTxWrapper(TransactionManager sessionManager) { + this.sessionManager = sessionManager; + } + + TransactionManager getTxManager() { + return sessionManager; + } + + @Override + public RESULT wrapInTransaction(CONTEXT executionContext, Function operation) { + LocalDateTime transactionStart = LocalDateTime.now(); + String sessionId = executionContext.getSessionId(); + ClientSession clientSession = sessionManager.startSession(sessionId); + String connectionInfo = getConnectionInfo(clientSession); + + try { + logger.debug("Starting MongoDB reactive transaction [connection={}]", connectionInfo); + clientSession.startTransaction(TransactionOptions.builder().build()); + executionContext.addDependency(new Dependency(ClientSession.class, clientSession, false)); + + try { + RESULT result = operation.apply(executionContext); + Duration transactionDuration = Duration.between(transactionStart, LocalDateTime.now()); + if (result instanceof FailedStep) { + logger.info("Rolling back MongoDB reactive transaction due to failed step [duration={}]", formatDuration(transactionDuration)); + PublisherSync.complete(clientSession.abortTransaction()); + logger.debug("MongoDB reactive transaction rollback completed successfully [duration={}]", formatDuration(transactionDuration)); + } else { + logger.debug("Committing successful MongoDB reactive transaction [duration={}]", formatDuration(transactionDuration)); + PublisherSync.complete(clientSession.commitTransaction()); + logger.debug("MongoDB reactive transaction commit completed successfully [duration={}]", formatDuration(transactionDuration)); + } + return result; + } catch (Exception e) { + Duration failureDuration = Duration.between(transactionStart, LocalDateTime.now()); + logger.debug("MongoDB reactive transaction failed, attempting rollback [duration={} error={}]", + formatDuration(failureDuration), e.getMessage()); + + DatabaseTransactionException.RollbackStatus rollbackStatus; + try { + PublisherSync.complete(clientSession.abortTransaction()); + rollbackStatus = DatabaseTransactionException.RollbackStatus.SUCCESS; + logger.info("MongoDB reactive transaction rollback completed successfully after failure [duration={}]", + formatDuration(failureDuration)); + } catch (Exception rollbackEx) { + rollbackStatus = DatabaseTransactionException.RollbackStatus.FAILED; + logger.debug("MongoDB reactive transaction rollback failed [duration={} rollback_error={}]", + formatDuration(failureDuration), rollbackEx.getMessage(), rollbackEx); + } + throw new DatabaseTransactionException( + "MongoDB reactive transaction failed during operation execution", + DatabaseTransactionException.TransactionState.FAILED, + null, // isolation level not applicable to MongoDB + null, // timeout not available + failureDuration, + rollbackStatus, + null, // specific operation not available at this level + connectionInfo, + e); + } + } finally { + try { + clientSession.close(); + } finally { + sessionManager.closeSession(sessionId); + } + } + } + + private String getConnectionInfo(ClientSession session) { + try { + return String.format("session_%s", session.getServerSession().getIdentifier()); + } catch (Exception e) { + return "connection_info_unavailable"; + } + } + + private String formatDuration(Duration duration) { + long millis = duration.toMillis(); + if (millis < 1000) { + return millis + "ms"; + } else if (millis < 60000) { + return String.format("%.1fs", millis / 1000.0); + } else { + return String.format("%.1fm", millis / 60000.0); + } + } +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveAuditMarkerTest.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveAuditMarkerTest.java new file mode 100644 index 000000000..51a317b97 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveAuditMarkerTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive; + +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType; +import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark; +import io.flamingock.internal.core.transaction.TransactionManager; +import io.flamingock.reactive.util.PublisherSync; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Testcontainers +public class MongoDBReactiveAuditMarkerTest { + + private static final String DB_NAME = "test"; + private static final String MARKER_COLLECTION = "flamingockAuditMarkerTest"; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + private static MongoClient mongoClient; + private static MongoDatabase database; + + private TransactionManager txManager; + private MongoDBReactiveAuditMarker marker; + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(mongoDBContainer.getConnectionString()); + database = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void beforeEach() { + txManager = new TransactionManager<>(() -> PublisherSync.first(mongoClient.startSession())); + marker = MongoDBReactiveAuditMarker.builder(database, txManager) + .setCollectionName(MARKER_COLLECTION) + .withAutoCreate(true) + .build(); + clearAll(); + } + + @AfterEach + void afterEach() { + clearAll(); + } + + @Test + @DisplayName("mark() persists each mark and listAll() returns them with the right contents") + void markPersistsAndIsReadableViaListAll() { + String changeId1 = "change-1"; + String changeId2 = "change-2"; + + startSession(changeId1); + startSession(changeId2); + marker.mark(new TargetSystemAuditMark(changeId1, TargetSystemAuditMarkType.APPLIED)); + marker.mark(new TargetSystemAuditMark(changeId2, TargetSystemAuditMarkType.ROLLED_BACK)); + + Set marks = marker.listAll(); + Assertions.assertEquals(2, marks.size()); + + Map byId = marks.stream() + .collect(Collectors.toMap(TargetSystemAuditMark::getChangeId, + TargetSystemAuditMark::getOperation)); + Assertions.assertEquals(TargetSystemAuditMarkType.APPLIED, byId.get(changeId1)); + Assertions.assertEquals(TargetSystemAuditMarkType.ROLLED_BACK, byId.get(changeId2)); + } + + @Test + @DisplayName("clearMark() removes only the targeted mark") + void clearMarkRemovesOnlyTheTargetedMark() { + String changeId1 = "change-1"; + String changeId2 = "change-2"; + + startSession(changeId1); + startSession(changeId2); + marker.mark(new TargetSystemAuditMark(changeId1, TargetSystemAuditMarkType.APPLIED)); + marker.mark(new TargetSystemAuditMark(changeId2, TargetSystemAuditMarkType.APPLIED)); + + marker.clearMark(changeId1); + + Set marks = marker.listAll(); + Assertions.assertEquals(1, marks.size()); + Assertions.assertEquals(changeId2, marks.iterator().next().getChangeId()); + } + + private void startSession(String changeId) { + txManager.startSession(changeId); + } + + private void clearAll() { + PublisherSync.complete(database.getCollection(MARKER_COLLECTION).drop()); + marker = MongoDBReactiveAuditMarker.builder(database, txManager) + .setCollectionName(MARKER_COLLECTION) + .withAutoCreate(true) + .build(); + } +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystemE2ETest.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystemE2ETest.java new file mode 100644 index 000000000..539af5d1f --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystemE2ETest.java @@ -0,0 +1,295 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.external.TargetSystem; +import io.flamingock.targetsystem.mongodb.reactive.changes.happypath._001__HappyCreateClientsCollectionChange; +import io.flamingock.targetsystem.mongodb.reactive.changes.happypath._002__HappyInsertClientsChange; +import io.flamingock.targetsystem.mongodb.reactive.changes.unhappypath._001__UnhappyCreateClientsCollectionChange; +import io.flamingock.targetsystem.mongodb.reactive.changes.unhappypath._002__UnhappyInsertClientsChange; +import io.flamingock.common.test.cloud.AuditRequestExpectation; +import io.flamingock.common.test.cloud.MockRunnerServer; +import io.flamingock.common.test.cloud.execution.ExecutionContinueRequestResponseMock; +import io.flamingock.common.test.cloud.execution.ExecutionPlanRequestResponseMock; +import io.flamingock.common.test.cloud.mock.MockRequestResponseChange; +import io.flamingock.common.test.cloud.prototype.PrototypeClientSubmission; +import io.flamingock.common.test.cloud.prototype.PrototypeStage; +import io.flamingock.internal.util.Trio; +import io.flamingock.internal.util.constants.CommunityPersistenceConstants; +import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType; +import io.flamingock.internal.core.builder.FlamingockFactory; +import io.flamingock.internal.core.builder.CloudChangeRunnerBuilder; +import io.flamingock.internal.common.core.metadata.MetadataLoader; +import io.flamingock.internal.core.operation.OperationException; +import io.flamingock.internal.core.builder.runner.Runner; +import org.junit.jupiter.api.*; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.Collections; + +import static io.flamingock.cloud.api.vo.CloudAuditStatus.APPLIED; +import static io.flamingock.cloud.api.vo.CloudAuditStatus.FAILED; +import static io.flamingock.cloud.api.vo.CloudAuditStatus.ROLLED_BACK; + +@Testcontainers +public class MongoDBReactiveTargetSystemE2ETest { + + private static final Logger logger = LoggerFactory.getLogger(MongoDBReactiveTargetSystemE2ETest.class); + + private static final String DB_NAME = "test"; + private static final String CLIENTS_COLLECTION = "clientCollection"; + + private static MongoClient mongoClient; + private static MongoDatabase testDatabase; + private static MongoDBTestHelper mongoDBTestHelper; + + private final String apiToken = "FAKE_API_TOKEN"; + private final long organisationId = 1L; + private final String organisationName = "MyOrganisation"; + private final long projectId = 2L; + private final String projectName = "MyOrganisation"; + private final String serviceName = "clients-service"; + private final String environmentName = "development"; + private final long serviceId = 3L; + private final long environmentId = 4L; + private final long credentialId = 5L; + private final int runnerServerPort = 8888; + private final String jwt = "fake_jwt"; + + private MockRunnerServer mockRunnerServer; + private CloudChangeRunnerBuilder flamingockBuilder; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + testDatabase = mongoClient.getDatabase(DB_NAME); + mongoDBTestHelper = new MongoDBTestHelper(testDatabase); + } + + @BeforeEach + void beforeEach() { + mockRunnerServer = new MockRunnerServer() + .setServerPort(runnerServerPort) + .setOrganisationId(organisationId) + .setOrganisationName(organisationName) + .setProjectId(projectId) + .setProjectName(projectName) + .setServiceId(serviceId) + .setServiceName(serviceName) + .setEnvironmentId(environmentId) + .setEnvironmentName(environmentName) + .setCredentialId(credentialId) + .setApiToken(apiToken) + .setJwt(jwt); + + flamingockBuilder = FlamingockFactory.getCloudBuilder() + .setApiToken(apiToken) + .setHost("http://localhost:" + runnerServerPort) + .setService(serviceName) + .setEnvironment(environmentName); + } + + @AfterEach + void afterEach() throws Exception { + //tear down + mockRunnerServer.stop(); + + ReactiveMongoTestHelper.complete(testDatabase.getCollection(CLIENTS_COLLECTION).drop()); + ReactiveMongoTestHelper.complete(testDatabase.getCollection(CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME).drop()); + } + + @Test + @DisplayName("Should follow the targetSystem lifecycle") + void happyPath() { + long executionId = 1L; + String stageName = "stage-1"; + + PrototypeClientSubmission prototypeClientSubmission = new PrototypeClientSubmission( + new PrototypeStage(stageName, 0) + .addChange("create-clients-collection", _001__HappyCreateClientsCollectionChange.class.getName(), "apply", false) + .addChange("insert-clients", _002__HappyInsertClientsChange.class.getName(), "apply", true) + ); + + //GIVEN + try (MockedStatic mocked = Mockito.mockStatic(MetadataLoader.class)) { + mockRunnerServer + .withClientSubmissionBase(prototypeClientSubmission) + .withExecutionPlanRequestsExpectation( + new ExecutionPlanRequestResponseMock(executionId), + new ExecutionContinueRequestResponseMock() + ).withAuditRequestsExpectation( + new AuditRequestExpectation(executionId, "create-clients-collection", APPLIED), + new AuditRequestExpectation(executionId, "insert-clients", APPLIED) + ).start(); + + + //WHEN + mocked.when(MetadataLoader::loadAggregated).thenReturn(PipelineTestHelper.getPreviewPipeline( + "stage-1", + new Trio<>(_001__HappyCreateClientsCollectionChange.class, Collections.singletonList(MongoDatabase.class)), + new Trio<>(_002__HappyInsertClientsChange.class, Collections.singletonList(MongoDatabase.class)) + )); + + + TargetSystem mongoDBTargetSystem = new MongoDBReactiveTargetSystem("mongodb-ts", mongoClient, DB_NAME); + + flamingockBuilder + .addTargetSystem(mongoDBTargetSystem) + .build() + .execute(); + + //THEN + mockRunnerServer.verifyAllCalls(); + + // check clients changes + mongoDBTestHelper.checkCount(testDatabase.getCollection(CLIENTS_COLLECTION), 1); + // check ongoing status + mongoDBTestHelper.checkOngoingChange(ongoingCount -> ongoingCount == 0); + } + } + + @Test + @DisplayName("Should rollback the ongoing deletion when a change fails") + void failedChanges() { + long executionId = 1L; + String stageName = "stage-1"; + + PrototypeClientSubmission prototypeClientSubmission = new PrototypeClientSubmission( + new PrototypeStage(stageName, 0) + .addChange("create-clients-collection", _001__UnhappyCreateClientsCollectionChange.class.getName(), "apply", false) + .addChange("insert-clients", _002__UnhappyInsertClientsChange.class.getName(), "apply", true) + ); + + //GIVEN + try ( + MockedStatic mocked = Mockito.mockStatic(MetadataLoader.class) + ) { + MongoDBReactiveTargetSystem mongoTargetSystem = new MongoDBReactiveTargetSystem("mongodb-ts", mongoClient, DB_NAME); + + mockRunnerServer + .withClientSubmissionBase(prototypeClientSubmission) + .withExecutionPlanRequestsExpectation( + new ExecutionPlanRequestResponseMock(executionId), + new ExecutionContinueRequestResponseMock() + ).withAuditRequestsExpectation( + new AuditRequestExpectation(executionId, "create-clients-collection", APPLIED), + new AuditRequestExpectation(executionId, "insert-clients", FAILED), + new AuditRequestExpectation(executionId, "insert-clients", ROLLED_BACK) + ).start(); + + MongoDBReactiveTargetSystem mongoSyncCloudTransactioner = Mockito.spy(mongoTargetSystem); + + //WHEN + mocked.when(MetadataLoader::loadAggregated).thenReturn(PipelineTestHelper.getPreviewPipeline( + "stage-1", + new Trio<>(_001__UnhappyCreateClientsCollectionChange.class, Collections.singletonList(MongoDatabase.class)), + new Trio<>(_002__UnhappyInsertClientsChange.class, Collections.singletonList(MongoDatabase.class)) + )); + + TargetSystem mongoDBTargetSystem = new MongoDBReactiveTargetSystem("mongodb-ts", mongoClient, DB_NAME); + + Runner runner = flamingockBuilder + .addTargetSystem(mongoDBTargetSystem) + .build(); + + //THEN + OperationException ex = Assertions.assertThrows(OperationException.class, runner::run); + + mockRunnerServer.verifyAllCalls(); + + // check clients changes + mongoDBTestHelper.checkCount(testDatabase.getCollection(CLIENTS_COLLECTION), 0); + + // check ongoing status + mongoDBTestHelper.checkEmptyTargetSystemAudiMarker(); + } + } + + + //TODO verify the server is called with the right parameters. among other, it sends the ongoing status + @Test + @Disabled("adapt when adding cloud support") + @DisplayName("Should send ongoing change in execution when is present in local database") + void shouldSendOngoingChangeInExecutionPlan() { + long executionId = 1L; + String stageName = "stage-1"; + + PrototypeClientSubmission prototypeClientSubmission = new PrototypeClientSubmission( + new PrototypeStage(stageName, 0) + .addChange("create-clients-collection", _001__HappyCreateClientsCollectionChange.class.getName(), "apply", false) + .addChange("insert-clients", _002__HappyInsertClientsChange.class.getName(), "apply", true) + ); + + //GIVEN + try ( + MockedStatic mocked = Mockito.mockStatic(MetadataLoader.class) + + ) { + MongoDBReactiveTargetSystem mongoTargetSystem = new MongoDBReactiveTargetSystem("mongodb-ts", mongoClient, DB_NAME); + + mongoDBTestHelper.insertOngoingExecution("insert-clients"); + mockRunnerServer + .withClientSubmissionBase(prototypeClientSubmission) + .withExecutionPlanRequestsExpectation( + new ExecutionPlanRequestResponseMock(executionId, new MockRequestResponseChange("insert-clients", TargetSystemAuditMarkType.APPLIED)), + new ExecutionContinueRequestResponseMock() + ).withAuditRequestsExpectation( + new AuditRequestExpectation(executionId, "create-clients-collection", APPLIED), + new AuditRequestExpectation(executionId, "insert-clients", APPLIED) + ).start(); + + + //WHEN + mocked.when(MetadataLoader::loadAggregated).thenReturn(PipelineTestHelper.getPreviewPipeline( + "stage-1", + new Trio<>(_001__HappyCreateClientsCollectionChange.class, Collections.singletonList(MongoDatabase.class)), + new Trio<>(_002__HappyInsertClientsChange.class, Collections.singletonList(MongoDatabase.class)) + )); + TargetSystem mongoDBTargetSystem = new MongoDBReactiveTargetSystem("mongodb-ts", mongoClient, DB_NAME); + + flamingockBuilder + .addTargetSystem(mongoDBTargetSystem) + .build() + .execute(); + + //THEN + mockRunnerServer.verifyAllCalls(); + + // check clients changes + mongoDBTestHelper.checkCount(testDatabase.getCollection(CLIENTS_COLLECTION), 1); + // check ongoing status + mongoDBTestHelper.checkOngoingChange(ongoingCount -> ongoingCount == 0); + } + } +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystemTest.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystemTest.java new file mode 100644 index 000000000..8605f03eb --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystemTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.internal.common.core.context.ContextResolver; +import io.flamingock.internal.core.builder.FlamingockEdition; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class MongoDBReactiveTargetSystemTest { + + @Test + void initializesReactiveDependencies() { + MongoClient client = mock(MongoClient.class); + MongoDatabase database = mock(MongoDatabase.class); + when(client.getDatabase("test")).thenReturn(database); + when(database.withReadConcern(ReadConcern.MAJORITY)).thenReturn(database); + when(database.withReadPreference(ReadPreference.primary())).thenReturn(database); + when(database.withWriteConcern(WriteConcern.MAJORITY.withJournal(true))).thenReturn(database); + + ContextResolver baseContext = mock(ContextResolver.class); + when(baseContext.getDependencyValue(FlamingockEdition.class)) + .thenReturn(Optional.of(FlamingockEdition.COMMUNITY)); + + MongoDBReactiveTargetSystem targetSystem = + new MongoDBReactiveTargetSystem("mongodb", client, "test"); + targetSystem.initialize(baseContext); + + assertSame(client, targetSystem.getContext().getRequiredDependencyValue(MongoClient.class)); + assertSame(database, targetSystem.getContext().getRequiredDependencyValue(MongoDatabase.class)); + assertSame(database, targetSystem.getMongoDatabase()); + } +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBTestHelper.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBTestHelper.java new file mode 100644 index 000000000..283964028 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBTestHelper.java @@ -0,0 +1,102 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive; + +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.internal.util.constants.CommunityPersistenceConstants; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; +import io.flamingock.internal.common.mongodb.MongoDBReactiveCollectionHelper; +import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType; +import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark; +import io.flamingock.internal.core.external.store.audit.domain.AuditContextBundle; +import io.flamingock.internal.common.mongodb.CollectionInitializator; +import io.flamingock.internal.common.mongodb.MongoDBAuditMapper; +import org.bson.Document; + +import java.util.HashSet; +import java.util.function.Predicate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MongoDBTestHelper { + + public final MongoDatabase mongoDatabase; + + private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document())); + + public MongoDBTestHelper(MongoDatabase mongoDatabase) { + this.mongoDatabase = mongoDatabase; + } + + public void insertOngoingExecution(String changeId) { + + MongoCollection onGoingChangesCollection = mongoDatabase.getCollection(CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME); + + CollectionInitializator initializer = new CollectionInitializator<>( + new MongoDBReactiveCollectionHelper(onGoingChangesCollection), + () -> new MongoDBDocumentHelper(new Document()), + new String[]{"changeId"} + ); + initializer.initialize(); + + + Document filter = new Document("changeId", changeId); + + Document newDocument = new Document("changeId", changeId) + .append("operation", AuditContextBundle.Operation.EXECUTION.toString()); + + ReactiveMongoTestHelper.complete(onGoingChangesCollection.updateOne( + filter, + new Document("$set", newDocument), + new com.mongodb.client.model.UpdateOptions().upsert(true))); + + checkEmptyTargetSystemAudiMarker(); + } + + public void checkCount(MongoCollection collection, int count) { + long result = ReactiveMongoTestHelper.collect(collection + .find()) + .stream() + .count(); + assertEquals(count, (int) result); + } + + public void checkEmptyTargetSystemAudiMarker() { + checkOngoingChange(result -> result == 0); + } + + public void checkOngoingChange(Predicate predicate) { + MongoCollection onGoingChangesCollection = mongoDatabase.getCollection("flamingockOnGoingChanges"); + + long result = ReactiveMongoTestHelper.collect(onGoingChangesCollection.find()) + .stream() + .map(MongoDBTestHelper::mapToOnGoingStatus) + .collect(java.util.stream.Collectors.toCollection(HashSet::new)) + .size(); + + assertTrue(predicate.test(result)); + } + + public static TargetSystemAuditMark mapToOnGoingStatus(Document document) { + TargetSystemAuditMarkType operation = TargetSystemAuditMarkType.valueOf(document.getString("operation")); + return new TargetSystemAuditMark(document.getString("changeId"), operation); + } + + + +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/PipelineTestHelper.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/PipelineTestHelper.java new file mode 100644 index 000000000..ad61beee9 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/PipelineTestHelper.java @@ -0,0 +1,162 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive; + +import io.flamingock.api.StageType; +import io.flamingock.api.annotations.TargetSystem; +import io.flamingock.internal.common.core.metadata.FlamingockMetadata; +import io.flamingock.internal.common.core.preview.PreviewConstructor; +import io.flamingock.internal.common.core.change.RecoveryDescriptor; +import io.flamingock.internal.common.core.change.TargetSystemDescriptor; +import io.flamingock.internal.core.change.loaded.ChangeOrderUtil; +import io.flamingock.internal.util.Pair; +import io.flamingock.internal.util.Trio; +import io.flamingock.api.annotations.Change; +import io.flamingock.internal.common.core.preview.CodePreviewChange; +import io.flamingock.internal.common.core.preview.PreviewMethod; +import io.flamingock.internal.common.core.preview.PreviewPipeline; +import io.flamingock.internal.common.core.preview.PreviewStage; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class PipelineTestHelper { + + + private static final Function, ChangeInfo> infoExtractor = c -> { + Change ann = c.getAnnotation(Change.class); + TargetSystem targetSystemAnn = c.getAnnotation(TargetSystem.class); + String targetSystemId = targetSystemAnn != null ? targetSystemAnn.id() : null; + String changeId = ann.id(); + String order = ChangeOrderUtil.getMatchedOrderFromClassName(changeId, null, c.getName()); + return new ChangeInfo(changeId, order, ann.author(), targetSystemId, ann.transactional()); + }; + + @NotNull + private static List getParameterTypes(List> second) { + return second + .stream() + .map(Class::getName) + .collect(Collectors.toList()); + } + + /** + * Builds a {@link PreviewPipeline} composed of a single {@link PreviewStage} containing one or more {@link CodePreviewChange}s. + *

+ * Each change is derived from a {@link Pair} where: + *

    + *
  • The first item is the {@link Class} annotated with {@link Change}
  • + *
  • The second item is a {@link List} of parameter types (as {@link Class}) expected by the method annotated with {@code @Apply}
  • + *
  • The third item is a {@link List} of parameter types (as {@link Class}) expected by the method annotated with {@code @Rollback}
  • + *
+ * + * @param changeDefinitions varargs of pairs containing change classes and their execution method parameters + * @return a {@link PreviewPipeline} ready for preview or testing + */ + @SafeVarargs + public static FlamingockMetadata getPreviewPipeline(String stageName, Trio, List>, List>>... changeDefinitions) { + + List changes = Arrays.stream(changeDefinitions) + .map(trio -> { + Function, ChangeInfo> extractor = infoExtractor; + ChangeInfo changeInfo = extractor.apply(trio.getFirst()); + PreviewMethod rollback = null; + if (trio.getThird() != null) { + rollback = new PreviewMethod("rollback", getParameterTypes(trio.getThird())); + } + + List changeList = new ArrayList<>(); + changeList.add(new CodePreviewChange( + changeInfo.getChangeId(), + changeInfo.getOrder(), + changeInfo.getAuthor(), + trio.getFirst().getName(), + null, + PreviewConstructor.getDefault(), + new PreviewMethod("apply", getParameterTypes(trio.getSecond())), + rollback, + false, + changeInfo.transactional, + false, + changeInfo.targetSystem, + RecoveryDescriptor.getDefault(), + false + )); + return changeList; + }) + .flatMap(List::stream) + .collect(Collectors.toList()); + + PreviewStage stage = new PreviewStage( + stageName, + StageType.DEFAULT, + "some description", + null, + null, + changes + ); + + PreviewPipeline previewPipeline = new PreviewPipeline(Collections.singletonList(stage)); + return new FlamingockMetadata(previewPipeline, null, null); + } + + @SafeVarargs + public static FlamingockMetadata getPreviewPipeline(Trio, List>, List>>... changeDefinitions) { + return getPreviewPipeline("default-stage-name", changeDefinitions); + } + + static class ChangeInfo { + private final String changeId; + private final String order; + private final String author; + private final TargetSystemDescriptor targetSystem; + private final boolean transactional; + + public ChangeInfo(String changeId, String order, String author,String targetSystemId, boolean transactional) { + this.changeId = changeId; + this.order = order; + this.author = author; + this.targetSystem = new TargetSystemDescriptor(targetSystemId); + this.transactional = transactional; + } + + public String getChangeId() { + return changeId; + } + + public String getOrder() { + return order; + } + + public String getAuthor() { + return author; + } + + public TargetSystemDescriptor getTargetSystem() { + return targetSystem; + } + + public boolean isTransactional() { + return transactional; + } + } + +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/ReactiveMongoTestHelper.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/ReactiveMongoTestHelper.java new file mode 100644 index 000000000..76b485f45 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/ReactiveMongoTestHelper.java @@ -0,0 +1,83 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive; + +import io.flamingock.internal.common.core.error.FlamingockException; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +public final class ReactiveMongoTestHelper { + + private ReactiveMongoTestHelper() { + } + + public static T first(Publisher publisher) { + List values = collect(publisher); + return values.isEmpty() ? null : values.get(0); + } + + public static List collect(Publisher publisher) { + List values = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + publisher.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T value) { + values.add(value); + } + + @Override + public void onError(Throwable throwable) { + error.set(throwable); + latch.countDown(); + } + + @Override + public void onComplete() { + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new FlamingockException(e); + } + Throwable throwable = error.get(); + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } + if (throwable != null) { + throw new FlamingockException(throwable); + } + return values; + } + + public static void complete(Publisher publisher) { + collect(publisher); + } +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/happypath/_001__HappyCreateClientsCollectionChange.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/happypath/_001__HappyCreateClientsCollectionChange.java new file mode 100644 index 000000000..8878ee413 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/happypath/_001__HappyCreateClientsCollectionChange.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive.changes.happypath; + +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.NonLockGuarded; +import io.flamingock.api.annotations.TargetSystem; + +import static io.flamingock.targetsystem.mongodb.reactive.ReactiveMongoTestHelper.complete; + +@TargetSystem( id = "mongodb-ts") +@Change(id = "create-clients-collection", author = "aperezdieppa", transactional = false) +public class _001__HappyCreateClientsCollectionChange { + + @Apply + public void apply(@NonLockGuarded MongoDatabase mongoDatabase) { + complete(mongoDatabase.createCollection("clientCollection")); + } +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/happypath/_002__HappyInsertClientsChange.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/happypath/_002__HappyInsertClientsChange.java new file mode 100644 index 000000000..1b609a601 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/happypath/_002__HappyInsertClientsChange.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive.changes.happypath; + +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.NonLockGuarded; +import io.flamingock.api.annotations.TargetSystem; +import org.bson.Document; + +import static io.flamingock.targetsystem.mongodb.reactive.ReactiveMongoTestHelper.complete; + +@TargetSystem( id = "mongodb-ts") +@Change(id = "insert-clients", author = "aperezdieppa") +public class _002__HappyInsertClientsChange { + + @Apply + public void apply(@NonLockGuarded MongoDatabase mongoDatabase, @NonLockGuarded ClientSession clientSession) { + MongoCollection collection = mongoDatabase.getCollection("clientCollection"); + complete(collection.insertOne(clientSession, new Document().append("Name", "Federico"))); + } +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/unhappypath/_001__UnhappyCreateClientsCollectionChange.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/unhappypath/_001__UnhappyCreateClientsCollectionChange.java new file mode 100644 index 000000000..898716def --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/unhappypath/_001__UnhappyCreateClientsCollectionChange.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive.changes.unhappypath; + +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.NonLockGuarded; +import io.flamingock.api.annotations.TargetSystem; + +import static io.flamingock.targetsystem.mongodb.reactive.ReactiveMongoTestHelper.complete; + +@TargetSystem( id = "mongodb-ts") +@Change(id = "create-clients-collection", author = "aperezdieppa", transactional = false) +public class _001__UnhappyCreateClientsCollectionChange { + + @Apply + public void apply(@NonLockGuarded MongoDatabase mongoDatabase) { + complete(mongoDatabase.createCollection("clientCollection")); + } +} diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/unhappypath/_002__UnhappyInsertClientsChange.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/unhappypath/_002__UnhappyInsertClientsChange.java new file mode 100644 index 000000000..012bcb8b6 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/unhappypath/_002__UnhappyInsertClientsChange.java @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.reactive.changes.unhappypath; + +import com.mongodb.reactivestreams.client.ClientSession; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.NonLockGuarded; +import io.flamingock.api.annotations.TargetSystem; +import org.bson.Document; + +import static io.flamingock.targetsystem.mongodb.reactive.ReactiveMongoTestHelper.complete; + +@TargetSystem( id = "mongodb-ts") +@Change(id = "insert-clients", author = "aperezdieppa") +public class _002__UnhappyInsertClientsChange { + + @Apply + public void apply(@NonLockGuarded MongoDatabase mongoDatabase, @NonLockGuarded ClientSession clientSession) { + MongoCollection collection = mongoDatabase.getCollection("clientCollection"); + complete(collection.insertOne(clientSession, new Document().append("name", "Should Have Been Rolled Back"))); + throw new RuntimeException("Intended exception"); + } +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/build.gradle.kts b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/build.gradle.kts new file mode 100644 index 000000000..298cde8af --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.kotlin.gradle.utils.extendsFrom + +val versions = mapOf( + "mongodb" to "4.0.0", + "spring-data" to "3.1.4" +) + +dependencies { + api(project(":core:flamingock-core")) + api(project(":utils:flamingock-reactive-util")) + api(project(":core:target-systems:flamingock-mongodb-reactive-externalsystem-api")) + + compileOnly("org.mongodb:mongodb-driver-reactivestreams:${versions["mongodb"]}") + compileOnly("org.springframework.data:spring-data-mongodb:${versions["spring-data"]}") + compileOnly("io.projectreactor:reactor-core:3.4.34") + + testImplementation("org.testcontainers:testcontainers-mongodb:2.0.2") + testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.2") + testImplementation(project(":cloud:flamingock-cloud")) + testImplementation(project(":utils:test-util")) + testImplementation(project(":utils:mongodb-util")) + testImplementation(project(":utils:mongodb-reactive-util")) +} + +description = "MongoDB Spring Data reactive target system for Spring-integrated applications" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} + +configurations.testImplementation { + extendsFrom(configurations.compileOnly.get()) +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveAuditMarker.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveAuditMarker.java new file mode 100644 index 000000000..6435d9716 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveAuditMarker.java @@ -0,0 +1,126 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive; + +import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType; +import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark; +import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMarker; +import io.flamingock.internal.util.constants.CommunityPersistenceConstants; +import org.bson.Document; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.index.Index; +import org.springframework.data.mongodb.core.index.IndexInfo; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.springframework.data.mongodb.core.query.Criteria.where; + +/** + * Audit marker for the MongoDB Spring Data reactive target system. + *

+ * Writes participate in the active Spring-managed MongoDB transaction by going through + * {@link ReactiveMongoTemplate}. This keeps the Spring reactive abstraction intact and + * avoids exposing the underlying reactive {@code ClientSession} through the Flamingock + * {@code TransactionManager}. + */ +public class MongoDBSpringDataReactiveAuditMarker implements TargetSystemAuditMarker { + + public static final String OPERATION = "operation"; + private static final String CHANGE_ID = "changeId"; + + private final ReactiveMongoTemplate mongoTemplate; + private final String collectionName; + + public MongoDBSpringDataReactiveAuditMarker(ReactiveMongoTemplate mongoTemplate, String collectionName) { + this.mongoTemplate = mongoTemplate; + this.collectionName = collectionName; + } + + public static Builder builder(ReactiveMongoTemplate mongoTemplate) { + return new Builder(mongoTemplate); + } + + public static TargetSystemAuditMark mapToOnGoingStatus(Document document) { + TargetSystemAuditMarkType operation = TargetSystemAuditMarkType.valueOf(document.getString(OPERATION)); + return new TargetSystemAuditMark(document.getString(CHANGE_ID), operation); + } + + @Override + public Set listAll() { + return mongoTemplate.findAll(Document.class, collectionName) + .map(MongoDBSpringDataReactiveAuditMarker::mapToOnGoingStatus) + .collectList() + .block() + .stream() + .collect(Collectors.toSet()); + } + + @Override + public void clearMark(String changeId) { + mongoTemplate.remove(Query.query(where(CHANGE_ID).is(changeId)), collectionName).block(); + } + + @Override + public void mark(TargetSystemAuditMark auditMark) { + Query query = Query.query(where(CHANGE_ID).is(auditMark.getChangeId())); + Update update = new Update() + .set(CHANGE_ID, auditMark.getChangeId()) + .set(OPERATION, auditMark.getOperation().name()); + mongoTemplate.upsert(query, update, collectionName).block(); + } + + public static class Builder { + + private final ReactiveMongoTemplate mongoTemplate; + private boolean autoCreate = true; + private String collectionName = CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME; + + public Builder(ReactiveMongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + public Builder setCollectionName(String collectionName) { + this.collectionName = collectionName; + return this; + } + + public Builder withAutoCreate(boolean autoCreate) { + this.autoCreate = autoCreate; + return this; + } + + public MongoDBSpringDataReactiveAuditMarker build() { + List indexes = mongoTemplate.indexOps(collectionName).getIndexInfo().collectList().block(); + boolean validIndex = indexes.stream() + .anyMatch(index -> index.isUnique() && index.isIndexForFields(java.util.Collections.singleton(CHANGE_ID))); + if (!validIndex && !autoCreate) { + throw new IllegalStateException( + "Index creation not allowed, but not created or wrongly created for collection " + collectionName); + } + if (!validIndex) { + mongoTemplate.indexOps(collectionName) + .ensureIndex(new Index().on(CHANGE_ID, Sort.Direction.ASC).unique()) + .block(); + } + return new MongoDBSpringDataReactiveAuditMarker(mongoTemplate, collectionName); + } + } +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystem.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystem.java new file mode 100644 index 000000000..39e42c720 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystem.java @@ -0,0 +1,139 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.externalsystem.mongodb.reactive.api.MongoDBReactiveExternalSystem; +import io.flamingock.internal.common.core.audit.AuditHistoryReader; +import io.flamingock.internal.common.core.audit.AuditReaderType; +import io.flamingock.internal.common.core.context.ContextResolver; +import io.flamingock.internal.common.core.error.FlamingockException; +import io.flamingock.internal.common.core.transaction.TransactionWrapper; +import io.flamingock.internal.core.builder.FlamingockEdition; +import io.flamingock.internal.core.external.targets.TransactionalTargetSystem; +import io.flamingock.internal.core.external.targets.mark.NoOpTargetSystemAuditMarker; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; + +import java.util.Optional; + +import static io.flamingock.internal.core.builder.FlamingockEdition.COMMUNITY; + +public class MongoDBSpringDataReactiveTargetSystem + extends TransactionalTargetSystem + implements MongoDBReactiveExternalSystem { + + private final ReactiveMongoTemplate mongoTemplate; + private WriteConcern writeConcern = WriteConcern.MAJORITY.withJournal(true); + private ReadConcern readConcern = ReadConcern.MAJORITY; + private ReadPreference readPreference = ReadPreference.primary(); + + private MongoDBSpringDataReactiveTxWrapper txWrapper; + + public MongoDBSpringDataReactiveTargetSystem(String id, ReactiveMongoTemplate mongoTemplate) { + super(id); + this.mongoTemplate = mongoTemplate; + } + + public MongoDBSpringDataReactiveTargetSystem withReadConcern(ReadConcern readConcern) { + this.readConcern = readConcern; + return this; + } + + public MongoDBSpringDataReactiveTargetSystem withReadPreference(ReadPreference readPreference) { + this.readPreference = readPreference; + return this; + } + + public MongoDBSpringDataReactiveTargetSystem withWriteConcern(WriteConcern writeConcern) { + this.writeConcern = writeConcern; + return this; + } + + public ReactiveMongoTemplate getMongoTemplate() { + return mongoTemplate; + } + + public WriteConcern getWriteConcern() { + return writeConcern; + } + + public ReadConcern getReadConcern() { + return readConcern; + } + + public ReadPreference getReadPreference() { + return readPreference; + } + + @Override + public MongoDatabase getMongoDatabase() { + if (mongoTemplate == null) { + throw new FlamingockException("TargetSystem is not initialized. The 'mongoTemplate' instance is required."); + } + return mongoTemplate.getMongoDatabaseFactory().getMongoDatabase().block(); + } + + @Override + public void initialize(ContextResolver baseContext) { + this.validate(); + targetSystemContext.addDependency(mongoTemplate); + + txWrapper = MongoDBSpringDataReactiveTxWrapper.builder() + .mongoTemplate(mongoTemplate) + .readConcern(readConcern) + .readPreference(readPreference) + .writeConcern(writeConcern) + .build(); + + FlamingockEdition edition = baseContext.getDependencyValue(FlamingockEdition.class).orElse(COMMUNITY); + auditMarker = edition == COMMUNITY + ? new NoOpTargetSystemAuditMarker(this.getId()) + : MongoDBSpringDataReactiveAuditMarker.builder(mongoTemplate).build(); + } + + private void validate() { + if (mongoTemplate == null) { + throw new FlamingockException("The 'mongoTemplate' instance is required."); + } + if (readConcern == null) { + throw new FlamingockException("The 'readConcern' property is required."); + } + if (readPreference == null) { + throw new FlamingockException("The 'readPreference' property is required."); + } + if (writeConcern == null) { + throw new FlamingockException("The 'writeConcern' property is required."); + } + } + + @Override + protected MongoDBSpringDataReactiveTargetSystem getSelf() { + return this; + } + + @Override + public TransactionWrapper getTxWrapper() { + return txWrapper; + } + + @Override + public Optional getAuditAuditReader(AuditReaderType type) { + return Optional.empty(); + } +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTxWrapper.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTxWrapper.java new file mode 100644 index 000000000..c97bdc59e --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTxWrapper.java @@ -0,0 +1,185 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; +import com.mongodb.TransactionOptions; +import com.mongodb.WriteConcern; +import com.mongodb.ClientSessionOptions; +import com.mongodb.reactivestreams.client.ClientSession; +import io.flamingock.internal.common.core.context.Dependency; +import io.flamingock.internal.common.core.context.RuntimeContext; +import io.flamingock.internal.common.core.error.DatabaseTransactionException; +import io.flamingock.internal.common.core.transaction.TransactionWrapper; +import io.flamingock.internal.core.change.navigation.step.FailedStep; +import io.flamingock.internal.util.log.FlamingockLoggerFactory; +import org.slf4j.Logger; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.function.Function; + +public class MongoDBSpringDataReactiveTxWrapper implements TransactionWrapper { + + private static final Logger logger = FlamingockLoggerFactory.getLogger("SpringMongoReactiveTx"); + private final ReactiveMongoTemplate mongoTemplate; + private final TransactionOptions txOptions; + + private MongoDBSpringDataReactiveTxWrapper(ReactiveMongoTemplate mongoTemplate, TransactionOptions txOptions) { + this.mongoTemplate = mongoTemplate; + this.txOptions = txOptions; + } + + @Override + public RESULT wrapInTransaction(CONTEXT executionContext, Function operation) { + LocalDateTime transactionStart = LocalDateTime.now(); + ClientSession clientSession = null; + try { + logger.debug("Starting MongoDB Spring Data reactive transaction"); + + clientSession = mongoTemplate.getMongoDatabaseFactory() + .getSession(ClientSessionOptions.builder().build()) + .block(); + clientSession.startTransaction(txOptions); + + ReactiveMongoTemplate sessionMongoTemplate = new ReactiveMongoTemplate( + mongoTemplate.getMongoDatabaseFactory().withSession(clientSession), + mongoTemplate.getConverter()); + executionContext.addDependency(new Dependency(ReactiveMongoTemplate.class, sessionMongoTemplate)); + + try { + RESULT result = operation.apply(executionContext); + Duration transactionDuration = Duration.between(transactionStart, LocalDateTime.now()); + + if (result instanceof FailedStep) { + logger.info("Rolling back MongoDB Spring Data reactive transaction due to failed step [duration={}]", formatDuration(transactionDuration)); + abortTransaction(clientSession); + logger.debug("MongoDB Spring Data reactive transaction rollback completed successfully [duration={}]", formatDuration(transactionDuration)); + } else { + logger.debug("Committing successful MongoDB Spring Data reactive transaction [duration={}]", formatDuration(transactionDuration)); + commitTransaction(clientSession); + logger.debug("MongoDB Spring Data reactive transaction commit completed successfully [duration={}]", formatDuration(transactionDuration)); + } + return result; + + } catch (Exception e) { + Duration failureDuration = Duration.between(transactionStart, LocalDateTime.now()); + logger.debug("MongoDB Spring Data reactive transaction failed, attempting rollback [duration={} error={}]", + formatDuration(failureDuration), e.getMessage()); + abortTransaction(clientSession); + + throw new DatabaseTransactionException( + "MongoDB Spring Data reactive transaction failed during operation execution", + DatabaseTransactionException.TransactionState.FAILED, + null, // isolation level not applicable to MongoDB + null, // timeout not available + failureDuration, + DatabaseTransactionException.RollbackStatus.SUCCESS, // Spring handles rollback + null, // specific operation not available at this level + "Spring Data Reactive MongoDB", + e); + } + } catch (Exception e) { + Duration failureDuration = Duration.between(transactionStart, LocalDateTime.now()); + if (e instanceof DatabaseTransactionException) { + throw e; + } + throw new DatabaseTransactionException( + "MongoDB Spring Data reactive transaction failed to start or commit", + DatabaseTransactionException.TransactionState.FAILED, + null, // isolation level not applicable to MongoDB + null, // timeout not available + failureDuration, + DatabaseTransactionException.RollbackStatus.SUCCESS, // Spring handles rollback + null, // specific operation not available at this level + "Spring Data Reactive MongoDB", + e); + } finally { + executionContext.addDependency(new Dependency(ReactiveMongoTemplate.class, mongoTemplate)); + if (clientSession != null) { + clientSession.close(); + } + } + } + + private void commitTransaction(ClientSession clientSession) { + reactor.core.publisher.Mono.from(clientSession.commitTransaction()).block(); + } + + private void abortTransaction(ClientSession clientSession) { + reactor.core.publisher.Mono.from(clientSession.abortTransaction()).block(); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private ReadConcern readConcern = ReadConcern.MAJORITY; + private ReadPreference readPreference = ReadPreference.primary(); + private WriteConcern writeConcern = WriteConcern.MAJORITY.withJournal(true); + private ReactiveMongoTemplate mongoTemplate; + + private Builder() { + } + + public Builder readConcern(ReadConcern readConcern) { + this.readConcern = readConcern; + return this; + } + + public Builder readPreference(ReadPreference readPreference) { + this.readPreference = readPreference; + return this; + } + + public Builder writeConcern(WriteConcern writeConcern) { + this.writeConcern = writeConcern; + return this; + } + + public Builder mongoTemplate(ReactiveMongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + return this; + } + + public MongoDBSpringDataReactiveTxWrapper build() { + if (mongoTemplate == null) { + throw new IllegalArgumentException("ReactiveMongoTemplate is required"); + } + TransactionOptions txOptions = TransactionOptions.builder() + .readConcern(readConcern) + .readPreference(readPreference) + .writeConcern(writeConcern) + .build(); + return new MongoDBSpringDataReactiveTxWrapper(mongoTemplate, txOptions); + } + } + + private String formatDuration(Duration duration) { + long millis = duration.toMillis(); + if (millis < 1000) { + return millis + "ms"; + } else if (millis < 60000) { + return String.format("%.1fs", millis / 1000.0); + } else { + return String.format("%.1fm", millis / 60000.0); + } + } +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveAuditMarkerTest.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveAuditMarkerTest.java new file mode 100644 index 000000000..b766d261a --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveAuditMarkerTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive; + +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType; +import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Testcontainers +public class MongoDBSpringDataReactiveAuditMarkerTest { + + private static final String DB_NAME = "test"; + private static final String MARKER_COLLECTION = "flamingockAuditMarkerTest"; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + private static ReactiveMongoTemplate mongoTemplate; + + private MongoDBSpringDataReactiveAuditMarker marker; + + @BeforeAll + static void beforeAll() { + MongoClient mongoClient = MongoClients.create(mongoDBContainer.getConnectionString()); + mongoTemplate = new ReactiveMongoTemplate(new SimpleReactiveMongoDatabaseFactory(mongoClient, DB_NAME)); + } + + @BeforeEach + void beforeEach() { + marker = MongoDBSpringDataReactiveAuditMarker.builder(mongoTemplate) + .setCollectionName(MARKER_COLLECTION) + .withAutoCreate(true) + .build(); + clearAll(); + } + + @AfterEach + void afterEach() { + clearAll(); + } + + @Test + @DisplayName("mark() persists each mark and listAll() returns them with the right contents") + void markPersistsAndIsReadableViaListAll() { + String changeId1 = "change-1"; + String changeId2 = "change-2"; + + marker.mark(new TargetSystemAuditMark(changeId1, TargetSystemAuditMarkType.APPLIED)); + marker.mark(new TargetSystemAuditMark(changeId2, TargetSystemAuditMarkType.ROLLED_BACK)); + + Set marks = marker.listAll(); + Assertions.assertEquals(2, marks.size()); + + Map byId = marks.stream() + .collect(Collectors.toMap(TargetSystemAuditMark::getChangeId, + TargetSystemAuditMark::getOperation)); + Assertions.assertEquals(TargetSystemAuditMarkType.APPLIED, byId.get(changeId1)); + Assertions.assertEquals(TargetSystemAuditMarkType.ROLLED_BACK, byId.get(changeId2)); + } + + @Test + @DisplayName("clearMark() removes only the targeted mark") + void clearMarkRemovesOnlyTheTargetedMark() { + String changeId1 = "change-1"; + String changeId2 = "change-2"; + + marker.mark(new TargetSystemAuditMark(changeId1, TargetSystemAuditMarkType.APPLIED)); + marker.mark(new TargetSystemAuditMark(changeId2, TargetSystemAuditMarkType.APPLIED)); + + marker.clearMark(changeId1); + + Set marks = marker.listAll(); + Assertions.assertEquals(1, marks.size()); + Assertions.assertEquals(changeId2, marks.iterator().next().getChangeId()); + } + + private void clearAll() { + mongoTemplate.dropCollection(MARKER_COLLECTION).block(); + marker = MongoDBSpringDataReactiveAuditMarker.builder(mongoTemplate) + .setCollectionName(MARKER_COLLECTION) + .withAutoCreate(true) + .build(); + } +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystemE2ETest.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystemE2ETest.java new file mode 100644 index 000000000..dcae06ef6 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystemE2ETest.java @@ -0,0 +1,294 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.external.TargetSystem; +import io.flamingock.targetsystem.mongodb.springdata.reactive.changes.happypath._001__HappyCreateClientsCollectionChange; +import io.flamingock.targetsystem.mongodb.springdata.reactive.changes.happypath._002__HappyInsertClientsChange; +import io.flamingock.targetsystem.mongodb.springdata.reactive.changes.unhappypath._001__UnhappyCreateClientsCollectionChange; +import io.flamingock.targetsystem.mongodb.springdata.reactive.changes.unhappypath._002__UnhappyInsertClientsChange; +import io.flamingock.common.test.cloud.AuditRequestExpectation; +import io.flamingock.common.test.cloud.MockRunnerServer; +import io.flamingock.common.test.cloud.execution.ExecutionContinueRequestResponseMock; +import io.flamingock.common.test.cloud.execution.ExecutionPlanRequestResponseMock; +import io.flamingock.common.test.cloud.mock.MockRequestResponseChange; +import io.flamingock.common.test.cloud.prototype.PrototypeClientSubmission; +import io.flamingock.common.test.cloud.prototype.PrototypeStage; +import io.flamingock.internal.util.Trio; +import io.flamingock.internal.util.constants.CommunityPersistenceConstants; +import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType; +import io.flamingock.internal.core.builder.FlamingockFactory; +import io.flamingock.internal.core.builder.CloudChangeRunnerBuilder; +import io.flamingock.internal.common.core.metadata.MetadataLoader; +import io.flamingock.internal.core.operation.OperationException; +import io.flamingock.internal.core.builder.runner.Runner; +import org.junit.jupiter.api.*; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.Collections; + +import static io.flamingock.cloud.api.vo.CloudAuditStatus.APPLIED; +import static io.flamingock.cloud.api.vo.CloudAuditStatus.FAILED; +import static io.flamingock.cloud.api.vo.CloudAuditStatus.ROLLED_BACK; + +@Testcontainers +public class MongoDBSpringDataReactiveTargetSystemE2ETest { + + private static final Logger logger = LoggerFactory.getLogger(MongoDBSpringDataReactiveTargetSystemE2ETest.class); + + private static final String DB_NAME = "test"; + private static final String CLIENTS_COLLECTION = "clientCollection"; + + private static MongoDatabase testDatabase; + private static ReactiveMongoTemplate mongoTemplate; + private static MongoDBTestHelper mongoDBTestHelper; + + private final String apiToken = "FAKE_API_TOKEN"; + private final long organisationId = 1L; + private final String organisationName = "MyOrganisation"; + private final long projectId = 2L; + private final String projectName = "MyOrganisation"; + private final String serviceName = "clients-service"; + private final String environmentName = "development"; + private final long serviceId = 3L; + private final long environmentId = 4L; + private final long credentialId = 5L; + private final int runnerServerPort = 8888; + private final String jwt = "fake_jwt"; + + private MockRunnerServer mockRunnerServer; + private CloudChangeRunnerBuilder flamingockBuilder; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + MongoClient mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + testDatabase = mongoClient.getDatabase(DB_NAME); + mongoTemplate = new ReactiveMongoTemplate(new SimpleReactiveMongoDatabaseFactory(mongoClient, DB_NAME)); + mongoDBTestHelper = new MongoDBTestHelper(testDatabase); + } + + @BeforeEach + void beforeEach() { + mockRunnerServer = new MockRunnerServer() + .setServerPort(runnerServerPort) + .setOrganisationId(organisationId) + .setOrganisationName(organisationName) + .setProjectId(projectId) + .setProjectName(projectName) + .setServiceId(serviceId) + .setServiceName(serviceName) + .setEnvironmentId(environmentId) + .setEnvironmentName(environmentName) + .setCredentialId(credentialId) + .setApiToken(apiToken) + .setJwt(jwt); + + flamingockBuilder = FlamingockFactory.getCloudBuilder() + .setApiToken(apiToken) + .setHost("http://localhost:" + runnerServerPort) + .setService(serviceName) + .setEnvironment(environmentName); + } + + @AfterEach + void afterEach() throws Exception { + //tear down + mockRunnerServer.stop(); + + ReactiveMongoTestHelper.complete(testDatabase.getCollection(CLIENTS_COLLECTION).drop()); + ReactiveMongoTestHelper.complete(testDatabase.getCollection(CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME).drop()); + } + + @Test + @DisplayName("Should follow the targetSystem lifecycle") + void happyPath() { + long executionId = 1L; + String stageName = "stage-1"; + + PrototypeClientSubmission prototypeClientSubmission = new PrototypeClientSubmission( + new PrototypeStage(stageName, 0) + .addChange("create-clients-collection", _001__HappyCreateClientsCollectionChange.class.getName(), "apply", false) + .addChange("insert-clients", _002__HappyInsertClientsChange.class.getName(), "apply", true) + ); + + //GIVEN + try (MockedStatic mocked = Mockito.mockStatic(MetadataLoader.class)) { + mockRunnerServer + .withClientSubmissionBase(prototypeClientSubmission) + .withExecutionPlanRequestsExpectation( + new ExecutionPlanRequestResponseMock(executionId), + new ExecutionContinueRequestResponseMock() + ).withAuditRequestsExpectation( + new AuditRequestExpectation(executionId, "create-clients-collection", APPLIED), + new AuditRequestExpectation(executionId, "insert-clients", APPLIED) + ).start(); + + + //WHEN + mocked.when(MetadataLoader::loadAggregated).thenReturn(PipelineTestHelper.getPreviewPipeline( + "stage-1", + new Trio<>(_001__HappyCreateClientsCollectionChange.class, Collections.singletonList(ReactiveMongoTemplate.class)), + new Trio<>(_002__HappyInsertClientsChange.class, Collections.singletonList(ReactiveMongoTemplate.class)) + )); + + + TargetSystem mongoDBTargetSystem = new MongoDBSpringDataReactiveTargetSystem("mongodb-ts", mongoTemplate); + + flamingockBuilder + .addTargetSystem(mongoDBTargetSystem) + .build() + .execute(); + + //THEN + mockRunnerServer.verifyAllCalls(); + + // check clients changes + mongoDBTestHelper.checkCount(testDatabase.getCollection(CLIENTS_COLLECTION), 1); + // check ongoing status + mongoDBTestHelper.checkOngoingChange(ongoingCount -> ongoingCount == 0); + } + } + + @Test + @DisplayName("Should rollback the ongoing deletion when a change fails") + void failedChanges() { + long executionId = 1L; + String stageName = "stage-1"; + + PrototypeClientSubmission prototypeClientSubmission = new PrototypeClientSubmission( + new PrototypeStage(stageName, 0) + .addChange("create-clients-collection", _001__UnhappyCreateClientsCollectionChange.class.getName(), "apply", false) + .addChange("insert-clients", _002__UnhappyInsertClientsChange.class.getName(), "apply", true) + ); + + //GIVEN + try ( + MockedStatic mocked = Mockito.mockStatic(MetadataLoader.class) + ) { + mockRunnerServer + .withClientSubmissionBase(prototypeClientSubmission) + .withExecutionPlanRequestsExpectation( + new ExecutionPlanRequestResponseMock(executionId), + new ExecutionContinueRequestResponseMock() + ).withAuditRequestsExpectation( + new AuditRequestExpectation(executionId, "create-clients-collection", APPLIED), + new AuditRequestExpectation(executionId, "insert-clients", FAILED), + new AuditRequestExpectation(executionId, "insert-clients", ROLLED_BACK) + ).start(); + + //WHEN + mocked.when(MetadataLoader::loadAggregated).thenReturn(PipelineTestHelper.getPreviewPipeline( + "stage-1", + new Trio<>(_001__UnhappyCreateClientsCollectionChange.class, Collections.singletonList(ReactiveMongoTemplate.class)), + new Trio<>(_002__UnhappyInsertClientsChange.class, Collections.singletonList(ReactiveMongoTemplate.class)) + )); + + TargetSystem mongoDBTargetSystem = new MongoDBSpringDataReactiveTargetSystem("mongodb-ts", mongoTemplate); + + Runner runner = flamingockBuilder + .addTargetSystem(mongoDBTargetSystem) + .build(); + + //THEN + OperationException ex = Assertions.assertThrows(OperationException.class, runner::run); + + mockRunnerServer.verifyAllCalls(); + + // check clients changes + mongoDBTestHelper.checkCount(testDatabase.getCollection(CLIENTS_COLLECTION), 0); + + // check ongoing status + mongoDBTestHelper.checkEmptyTargetSystemAudiMarker(); + } + } + + + //TODO verify the server is called with the right parameters. among other, it sends the ongoing status + @Test + @Disabled("adapt when adding cloud support") + @DisplayName("Should send ongoing change in execution when is present in local database") + void shouldSendOngoingChangeInExecutionPlan() { + long executionId = 1L; + String stageName = "stage-1"; + + PrototypeClientSubmission prototypeClientSubmission = new PrototypeClientSubmission( + new PrototypeStage(stageName, 0) + .addChange("create-clients-collection", _001__HappyCreateClientsCollectionChange.class.getName(), "apply", false) + .addChange("insert-clients", _002__HappyInsertClientsChange.class.getName(), "apply", true) + ); + + //GIVEN + try ( + MockedStatic mocked = Mockito.mockStatic(MetadataLoader.class) + + ) { + MongoDBSpringDataReactiveTargetSystem mongoTargetSystem = new MongoDBSpringDataReactiveTargetSystem("mongodb-ts", mongoTemplate); + + mongoDBTestHelper.insertOngoingExecution("insert-clients"); + mockRunnerServer + .withClientSubmissionBase(prototypeClientSubmission) + .withExecutionPlanRequestsExpectation( + new ExecutionPlanRequestResponseMock(executionId, new MockRequestResponseChange("insert-clients", TargetSystemAuditMarkType.APPLIED)), + new ExecutionContinueRequestResponseMock() + ).withAuditRequestsExpectation( + new AuditRequestExpectation(executionId, "create-clients-collection", APPLIED), + new AuditRequestExpectation(executionId, "insert-clients", APPLIED) + ).start(); + + + //WHEN + mocked.when(MetadataLoader::loadAggregated).thenReturn(PipelineTestHelper.getPreviewPipeline( + "stage-1", + new Trio<>(_001__HappyCreateClientsCollectionChange.class, Collections.singletonList(ReactiveMongoTemplate.class)), + new Trio<>(_002__HappyInsertClientsChange.class, Collections.singletonList(ReactiveMongoTemplate.class)) + )); + TargetSystem mongoDBTargetSystem = new MongoDBSpringDataReactiveTargetSystem("mongodb-ts", mongoTemplate); + + flamingockBuilder + .addTargetSystem(mongoDBTargetSystem) + .build() + .execute(); + + //THEN + mockRunnerServer.verifyAllCalls(); + + // check clients changes + mongoDBTestHelper.checkCount(testDatabase.getCollection(CLIENTS_COLLECTION), 1); + // check ongoing status + mongoDBTestHelper.checkOngoingChange(ongoingCount -> ongoingCount == 0); + } + } +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystemTest.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystemTest.java new file mode 100644 index 000000000..849727da1 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystemTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive; + +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.internal.common.core.context.ContextResolver; +import io.flamingock.internal.core.builder.FlamingockEdition; +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import reactor.core.publisher.Mono; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class MongoDBSpringDataReactiveTargetSystemTest { + + @Test + void initializesReactiveMongoTemplateDependency() { + ReactiveMongoTemplate template = mock(ReactiveMongoTemplate.class); + ReactiveMongoDatabaseFactory databaseFactory = mock(ReactiveMongoDatabaseFactory.class); + MongoDatabase database = mock(MongoDatabase.class); + when(template.getMongoDatabaseFactory()).thenReturn(databaseFactory); + when(databaseFactory.getMongoDatabase()).thenReturn(Mono.just(database)); + + ContextResolver baseContext = mock(ContextResolver.class); + when(baseContext.getDependencyValue(FlamingockEdition.class)) + .thenReturn(Optional.of(FlamingockEdition.COMMUNITY)); + + MongoDBSpringDataReactiveTargetSystem targetSystem = + new MongoDBSpringDataReactiveTargetSystem("mongodb", template); + targetSystem.initialize(baseContext); + + assertSame(template, targetSystem.getContext().getRequiredDependencyValue(ReactiveMongoTemplate.class)); + assertSame(database, targetSystem.getMongoDatabase()); + } +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBTestHelper.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBTestHelper.java new file mode 100644 index 000000000..8ce3e9fda --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBTestHelper.java @@ -0,0 +1,99 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive; + +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType; +import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark; +import io.flamingock.internal.core.external.store.audit.domain.AuditContextBundle; +import io.flamingock.internal.common.mongodb.CollectionInitializator; +import io.flamingock.internal.common.mongodb.MongoDBAuditMapper; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; +import io.flamingock.internal.common.mongodb.MongoDBReactiveCollectionHelper; +import io.flamingock.internal.util.constants.CommunityPersistenceConstants; +import org.bson.Document; + +import java.util.HashSet; +import java.util.function.Predicate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MongoDBTestHelper { + + public final MongoDatabase mongoDatabase; + + private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document())); + + public MongoDBTestHelper(MongoDatabase mongoDatabase) { + this.mongoDatabase = mongoDatabase; + } + + public void insertOngoingExecution(String changeId) { + + MongoCollection onGoingChangesCollection = mongoDatabase.getCollection(CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME); + + CollectionInitializator initializer = new CollectionInitializator<>( + new MongoDBReactiveCollectionHelper(onGoingChangesCollection), + () -> new MongoDBDocumentHelper(new Document()), + new String[]{"changeId"} + ); + initializer.initialize(); + + + Document filter = new Document("changeId", changeId); + + Document newDocument = new Document("changeId", changeId) + .append("operation", AuditContextBundle.Operation.EXECUTION.toString()); + + ReactiveMongoTestHelper.complete(onGoingChangesCollection.updateOne( + filter, + new Document("$set", newDocument), + new com.mongodb.client.model.UpdateOptions().upsert(true))); + + checkEmptyTargetSystemAudiMarker(); + } + + public void checkCount(MongoCollection collection, int count) { + long result = ReactiveMongoTestHelper.collect(collection.find()).stream().count(); + assertEquals(count, (int) result); + } + + public void checkEmptyTargetSystemAudiMarker() { + checkOngoingChange(result -> result == 0); + } + + public void checkOngoingChange(Predicate predicate) { + MongoCollection onGoingChangesCollection = mongoDatabase.getCollection("flamingockOnGoingChanges"); + + long result = ReactiveMongoTestHelper.collect(onGoingChangesCollection.find()) + .stream() + .map(MongoDBTestHelper::mapToOnGoingStatus) + .collect(java.util.stream.Collectors.toCollection(HashSet::new)) + .size(); + + assertTrue(predicate.test(result)); + } + + public static TargetSystemAuditMark mapToOnGoingStatus(Document document) { + TargetSystemAuditMarkType operation = TargetSystemAuditMarkType.valueOf(document.getString("operation")); + return new TargetSystemAuditMark(document.getString("changeId"), operation); + } + + + +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/PipelineTestHelper.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/PipelineTestHelper.java new file mode 100644 index 000000000..ddc1c5a31 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/PipelineTestHelper.java @@ -0,0 +1,160 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive; + +import io.flamingock.api.StageType; +import io.flamingock.api.annotations.TargetSystem; +import io.flamingock.internal.common.core.metadata.FlamingockMetadata; +import io.flamingock.internal.common.core.preview.PreviewConstructor; +import io.flamingock.internal.common.core.change.RecoveryDescriptor; +import io.flamingock.internal.common.core.change.TargetSystemDescriptor; +import io.flamingock.internal.core.change.loaded.ChangeOrderUtil; +import io.flamingock.internal.util.Pair; +import io.flamingock.internal.util.Trio; +import io.flamingock.api.annotations.Change; +import io.flamingock.internal.common.core.preview.CodePreviewChange; +import io.flamingock.internal.common.core.preview.PreviewMethod; +import io.flamingock.internal.common.core.preview.PreviewPipeline; +import io.flamingock.internal.common.core.preview.PreviewStage; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class PipelineTestHelper { + + + private static final Function, ChangeInfo> infoExtractor = c -> { + Change ann = c.getAnnotation(Change.class); + TargetSystem targetSystemAnn = c.getAnnotation(TargetSystem.class); + String targetSystemId = targetSystemAnn != null ? targetSystemAnn.id() : null; + String changeId = ann.id(); + String order = ChangeOrderUtil.getMatchedOrderFromClassName(changeId, null, c.getName()); + return new ChangeInfo(changeId, order, ann.author(), targetSystemId, ann.transactional()); + }; + + @NotNull + private static List getParameterTypes(List> second) { + return second + .stream() + .map(Class::getName) + .collect(Collectors.toList()); + } + + /** + * Builds a {@link PreviewPipeline} composed of a single {@link PreviewStage} containing one or more {@link CodePreviewChange}s. + *

+ * Each change is derived from a {@link Pair} where: + *

    + *
  • The first item is the {@link Class} annotated with {@link Change}
  • + *
  • The second item is a {@link List} of parameter types (as {@link Class}) expected by the method annotated with {@code @Apply}
  • + *
  • The third item is a {@link List} of parameter types (as {@link Class}) expected by the method annotated with {@code @Rollback}
  • + *
+ * + * @param changeDefinitions varargs of pairs containing change classes and their execution method parameters + * @return a {@link PreviewPipeline} ready for preview or testing + */ + @SafeVarargs + public static FlamingockMetadata getPreviewPipeline(String stageName, Trio, List>, List>>... changeDefinitions) { + + List changes = Arrays.stream(changeDefinitions) + .map(trio -> { + Function, ChangeInfo> extractor = infoExtractor; + ChangeInfo changeInfo = extractor.apply(trio.getFirst()); + PreviewMethod rollback = null; + if (trio.getThird() != null) { + rollback = new PreviewMethod("rollback", getParameterTypes(trio.getThird())); + } + + List changeList = new ArrayList<>(); + changeList.add(new CodePreviewChange( + changeInfo.getChangeId(), + changeInfo.getOrder(), + changeInfo.getAuthor(), + trio.getFirst().getName(), + null, + PreviewConstructor.getDefault(), + new PreviewMethod("apply", getParameterTypes(trio.getSecond())), + rollback, + false, + changeInfo.transactional, + false, + changeInfo.targetSystem, + RecoveryDescriptor.getDefault(), + false + )); + return changeList; + }) + .flatMap(List::stream) + .collect(Collectors.toList()); + + PreviewStage stage = new PreviewStage( + stageName, + StageType.DEFAULT, + "some description", + null, + null, + changes + ); + + PreviewPipeline previewPipeline = new PreviewPipeline(Collections.singletonList(stage)); + return new FlamingockMetadata(previewPipeline, null, null); + } + + @SafeVarargs + public static FlamingockMetadata getPreviewPipeline(Trio, List>, List>>... changeDefinitions) { + return getPreviewPipeline("default-stage-name", changeDefinitions); + } + + static class ChangeInfo { + private final String changeId; + private final String order; + private final String author; + private final TargetSystemDescriptor targetSystem; + private final boolean transactional; + + public ChangeInfo(String changeId, String order, String author, String targetSystemId, boolean transactional) { + this.changeId = changeId; + this.order = order; + this.author = author; + this.targetSystem = new TargetSystemDescriptor(targetSystemId); + this.transactional = transactional; + } + + public String getChangeId() { + return changeId; + } + + public String getOrder() { + return order; + } + + public String getAuthor() { return author; } + + public TargetSystemDescriptor getTargetSystem() { + return targetSystem; + } + + public boolean isTransactional() { + return transactional; + } + } + +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/ReactiveMongoTestHelper.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/ReactiveMongoTestHelper.java new file mode 100644 index 000000000..98b4efd0a --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/ReactiveMongoTestHelper.java @@ -0,0 +1,83 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive; + +import io.flamingock.internal.common.core.error.FlamingockException; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +public final class ReactiveMongoTestHelper { + + private ReactiveMongoTestHelper() { + } + + public static T first(Publisher publisher) { + List values = collect(publisher); + return values.isEmpty() ? null : values.get(0); + } + + public static List collect(Publisher publisher) { + List values = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + publisher.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T value) { + values.add(value); + } + + @Override + public void onError(Throwable throwable) { + error.set(throwable); + latch.countDown(); + } + + @Override + public void onComplete() { + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new FlamingockException(e); + } + Throwable throwable = error.get(); + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } + if (throwable != null) { + throw new FlamingockException(throwable); + } + return values; + } + + public static void complete(Publisher publisher) { + collect(publisher); + } +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/happypath/_001__HappyCreateClientsCollectionChange.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/happypath/_001__HappyCreateClientsCollectionChange.java new file mode 100644 index 000000000..8540ce382 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/happypath/_001__HappyCreateClientsCollectionChange.java @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive.changes.happypath; + +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.NonLockGuarded; +import io.flamingock.api.annotations.TargetSystem; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; + +@TargetSystem( id = "mongodb-ts") +@Change(id = "create-clients-collection", author = "aperezdieppa", transactional = false) +public class _001__HappyCreateClientsCollectionChange { + + @Apply + public void apply(@NonLockGuarded ReactiveMongoTemplate mongoTemplate) { + mongoTemplate.createCollection("clientCollection").block(); + } +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/happypath/_002__HappyInsertClientsChange.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/happypath/_002__HappyInsertClientsChange.java new file mode 100644 index 000000000..ef5dab84c --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/happypath/_002__HappyInsertClientsChange.java @@ -0,0 +1,33 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive.changes.happypath; + +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.NonLockGuarded; +import io.flamingock.api.annotations.TargetSystem; +import org.bson.Document; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; + +@TargetSystem( id = "mongodb-ts") +@Change(id = "insert-clients", author = "aperezdieppa") +public class _002__HappyInsertClientsChange { + + @Apply + public void apply(@NonLockGuarded ReactiveMongoTemplate mongoTemplate) { + mongoTemplate.insert(new Document().append("Name", "Federico"), "clientCollection").block(); + } +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/unhappypath/_001__UnhappyCreateClientsCollectionChange.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/unhappypath/_001__UnhappyCreateClientsCollectionChange.java new file mode 100644 index 000000000..fb6cfad13 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/unhappypath/_001__UnhappyCreateClientsCollectionChange.java @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive.changes.unhappypath; + +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.NonLockGuarded; +import io.flamingock.api.annotations.TargetSystem; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; + +@TargetSystem( id = "mongodb-ts") +@Change(id = "create-clients-collection", author = "aperezdieppa", transactional = false) +public class _001__UnhappyCreateClientsCollectionChange { + + @Apply + public void apply(@NonLockGuarded ReactiveMongoTemplate mongoTemplate) { + mongoTemplate.createCollection("clientCollection").block(); + } +} diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/unhappypath/_002__UnhappyInsertClientsChange.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/unhappypath/_002__UnhappyInsertClientsChange.java new file mode 100644 index 000000000..bb7e39504 --- /dev/null +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/unhappypath/_002__UnhappyInsertClientsChange.java @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.targetsystem.mongodb.springdata.reactive.changes.unhappypath; + +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.NonLockGuarded; +import io.flamingock.api.annotations.TargetSystem; +import org.bson.Document; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; + +@TargetSystem( id = "mongodb-ts") +@Change(id = "insert-clients", author = "aperezdieppa") +public class _002__UnhappyInsertClientsChange { + + @Apply + public void apply(@NonLockGuarded ReactiveMongoTemplate mongoTemplate + ) { + mongoTemplate.insert(new Document().append("name", "Should Have Been Rolled Back"), "clientCollection").block(); + throw new RuntimeException("Intended exception"); + } +} diff --git a/core/target-systems/flamingock-mongodb-springdata-targetsystem/build.gradle.kts b/core/target-systems/flamingock-mongodb-springdata-targetsystem/build.gradle.kts index 90fd725ae..e20c51802 100644 --- a/core/target-systems/flamingock-mongodb-springdata-targetsystem/build.gradle.kts +++ b/core/target-systems/flamingock-mongodb-springdata-targetsystem/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { //Flamingock api(project(":core:flamingock-core")) implementation(project(":utils:mongodb-util")) + implementation(project(":utils:mongodb-sync-util")) implementation(project(":legacy:mongock-importer-mongodb")) api(project(":core:target-systems:flamingock-mongodb-externalsystem-api")) diff --git a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBSpringDataAuditMarker.java b/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBSpringDataAuditMarker.java index 7f460d310..f45f0e133 100644 --- a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBSpringDataAuditMarker.java +++ b/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBSpringDataAuditMarker.java @@ -22,7 +22,7 @@ import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType; import io.flamingock.internal.common.mongodb.CollectionInitializator; import io.flamingock.internal.common.mongodb.MongoDBSyncCollectionHelper; -import io.flamingock.internal.common.mongodb.MongoDBSyncDocumentHelper; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark; import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMarker; import io.flamingock.internal.util.constants.CommunityPersistenceConstants; @@ -113,9 +113,9 @@ public Builder withAutoCreate(boolean autoCreate) { public MongoDBSpringDataAuditMarker build() { MongoDatabase db = mongoTemplate.getDb(); MongoCollection collection = db.getCollection(collectionName); - CollectionInitializator initializer = new CollectionInitializator<>( + CollectionInitializator initializer = new CollectionInitializator<>( new MongoDBSyncCollectionHelper(collection), - () -> new MongoDBSyncDocumentHelper(new Document()), + () -> new MongoDBDocumentHelper(new Document()), new String[]{CHANGE_ID} ); if (autoCreate) { diff --git a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBTestHelper.java b/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBTestHelper.java index 422e69a99..02d84d8af 100644 --- a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBTestHelper.java +++ b/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBTestHelper.java @@ -22,9 +22,9 @@ import io.flamingock.internal.core.external.store.audit.domain.AuditContextBundle; import io.flamingock.internal.common.mongodb.CollectionInitializator; import io.flamingock.internal.common.mongodb.MongoDBAuditMapper; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; +import io.flamingock.internal.common.mongodb.MongoDBSyncCollectionHelper; import io.flamingock.internal.util.constants.CommunityPersistenceConstants; -import io.flamingock.targetsystem.mongodb.springdata.util.MongoDBSyncCollectionHelper; -import io.flamingock.targetsystem.mongodb.springdata.util.MongoDBSyncDocumentHelper; import org.bson.Document; import java.util.HashSet; @@ -37,7 +37,7 @@ public class MongoDBTestHelper { public final MongoDatabase mongoDatabase; - private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBSyncDocumentHelper(new Document())); + private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document())); public MongoDBTestHelper(MongoDatabase mongoDatabase) { this.mongoDatabase = mongoDatabase; @@ -47,9 +47,9 @@ public void insertOngoingExecution(String changeId) { MongoCollection onGoingChangesCollection = mongoDatabase.getCollection(CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME); - CollectionInitializator initializer = new CollectionInitializator<>( + CollectionInitializator initializer = new CollectionInitializator<>( new MongoDBSyncCollectionHelper(onGoingChangesCollection), - () -> new MongoDBSyncDocumentHelper(new Document()), + () -> new MongoDBDocumentHelper(new Document()), new String[]{"changeId"} ); initializer.initialize(); diff --git a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/util/MongoDBSyncCollectionHelper.java b/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/util/MongoDBSyncCollectionHelper.java deleted file mode 100644 index 9e7b4b5c3..000000000 --- a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/util/MongoDBSyncCollectionHelper.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.targetsystem.mongodb.springdata.util; - -import com.mongodb.client.MongoCollection; -import com.mongodb.client.model.IndexOptions; -import io.flamingock.internal.common.mongodb.CollectionHelper; -import io.flamingock.internal.common.mongodb.DocumentHelper; -import org.bson.Document; - - -public class MongoDBSyncCollectionHelper implements CollectionHelper { - - private final MongoCollection collection; - - - public MongoDBSyncCollectionHelper(MongoCollection collection) { - this.collection = collection; - } - - @Override - public String getCollectionName() { - return collection.getNamespace().getCollectionName(); - } - - @Override - public Iterable listIndexes() { - return collection.listIndexes().map(MongoDBSyncDocumentHelper::new); - } - - @Override - public String createUniqueIndex(MongoDBSyncDocumentHelper uniqueIndexDocument) { - return collection.createIndex(uniqueIndexDocument.getDocument(), new IndexOptions().unique(true)); - } - - @Override - public void dropIndex(String indexName) { - collection.dropIndex(indexName); - } - - @Override - public void deleteMany(MongoDBSyncDocumentHelper documentWrapper) { - collection.deleteMany(documentWrapper.getDocument()); - } - - -} diff --git a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/util/MongoDBSyncDocumentHelper.java b/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/util/MongoDBSyncDocumentHelper.java deleted file mode 100644 index 07dda7ccc..000000000 --- a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/util/MongoDBSyncDocumentHelper.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2023 Flamingock (https://www.flamingock.io) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.flamingock.targetsystem.mongodb.springdata.util; - -import io.flamingock.internal.common.mongodb.DocumentHelper; -import org.bson.Document; - -public class MongoDBSyncDocumentHelper implements DocumentHelper { - - private final Document document; - - public MongoDBSyncDocumentHelper(Document document) { - this.document = document; - } - - public Document getDocument() { - return document; - } - - @Override - public DocumentHelper append(String key, Object value) { - document.append(key, value); - return this; - } - - @Override - public Object get(String key) { - return document.get(key); - } - - @Override - public String getString(String key) { - return document.getString(key); - } - - @Override - public boolean containsKey(String key) { - return document.containsKey(key); - } - - @Override - public Boolean getBoolean(String key) { - return document.getBoolean(key); - } - - public boolean getBoolean(Object key, boolean defaultValue) { - return document.getBoolean(key, defaultValue); - } - - @Override - public int size() { - return document.size(); - } - - @Override - public DocumentHelper getWithWrapper(String key) { - return new MongoDBSyncDocumentHelper((Document) get(key)); - } -} diff --git a/core/target-systems/flamingock-mongodb-sync-targetsystem/build.gradle.kts b/core/target-systems/flamingock-mongodb-sync-targetsystem/build.gradle.kts index be504b274..5a22f2b21 100644 --- a/core/target-systems/flamingock-mongodb-sync-targetsystem/build.gradle.kts +++ b/core/target-systems/flamingock-mongodb-sync-targetsystem/build.gradle.kts @@ -3,6 +3,7 @@ dependencies { //Flamingock api(project(":core:flamingock-core")) implementation(project(":utils:mongodb-util")) + implementation(project(":utils:mongodb-sync-util")) implementation(project(":legacy:mongock-importer-mongodb")) api(project(":core:target-systems:flamingock-mongodb-externalsystem-api")) @@ -27,4 +28,4 @@ java { configurations.testImplementation { extendsFrom(configurations.compileOnly.get()) -} \ No newline at end of file +} diff --git a/core/target-systems/flamingock-mongodb-sync-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/sync/MongoDBSyncAuditMarker.java b/core/target-systems/flamingock-mongodb-sync-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/sync/MongoDBSyncAuditMarker.java index d0d927366..6b6d563d6 100644 --- a/core/target-systems/flamingock-mongodb-sync-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/sync/MongoDBSyncAuditMarker.java +++ b/core/target-systems/flamingock-mongodb-sync-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/sync/MongoDBSyncAuditMarker.java @@ -25,7 +25,7 @@ import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType; import io.flamingock.internal.common.mongodb.CollectionInitializator; import io.flamingock.internal.common.mongodb.MongoDBSyncCollectionHelper; -import io.flamingock.internal.common.mongodb.MongoDBSyncDocumentHelper; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark; import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMarker; import io.flamingock.internal.core.transaction.TransactionManager; @@ -133,9 +133,9 @@ public MongoDBSyncAuditMarker build() { // .withWriteConcern(writeConcern) ; - CollectionInitializator initializer = new CollectionInitializator<>( + CollectionInitializator initializer = new CollectionInitializator<>( new MongoDBSyncCollectionHelper(collection), - () -> new MongoDBSyncDocumentHelper(new Document()), + () -> new MongoDBDocumentHelper(new Document()), new String[]{CHANGE_ID} ); if (autoCreate) { diff --git a/core/target-systems/flamingock-mongodb-sync-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/sync/MongoDBTestHelper.java b/core/target-systems/flamingock-mongodb-sync-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/sync/MongoDBTestHelper.java index 0cc38f536..a881eb075 100644 --- a/core/target-systems/flamingock-mongodb-sync-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/sync/MongoDBTestHelper.java +++ b/core/target-systems/flamingock-mongodb-sync-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/sync/MongoDBTestHelper.java @@ -19,7 +19,7 @@ import com.mongodb.client.MongoDatabase; import io.flamingock.internal.util.constants.CommunityPersistenceConstants; import io.flamingock.internal.common.mongodb.MongoDBSyncCollectionHelper; -import io.flamingock.internal.common.mongodb.MongoDBSyncDocumentHelper; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType; import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark; import io.flamingock.internal.core.external.store.audit.domain.AuditContextBundle; @@ -37,7 +37,7 @@ public class MongoDBTestHelper { public final MongoDatabase mongoDatabase; - private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBSyncDocumentHelper(new Document())); + private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document())); public MongoDBTestHelper(MongoDatabase mongoDatabase) { this.mongoDatabase = mongoDatabase; @@ -47,9 +47,9 @@ public void insertOngoingExecution(String changeId) { MongoCollection onGoingChangesCollection = mongoDatabase.getCollection(CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME); - CollectionInitializator initializer = new CollectionInitializator<>( + CollectionInitializator initializer = new CollectionInitializator<>( new MongoDBSyncCollectionHelper(onGoingChangesCollection), - () -> new MongoDBSyncDocumentHelper(new Document()), + () -> new MongoDBDocumentHelper(new Document()), new String[]{"changeId"} ); initializer.initialize(); diff --git a/settings.gradle.kts b/settings.gradle.kts index 43dfc69c8..55f05cff1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -61,6 +61,11 @@ project(":community:flamingock-mongodb-sync-auditstore").name = "flamingock-mong project(":community:flamingock-mongodb-sync-auditstore").projectDir = file("community/flamingock-mongodb-sync-auditstore") +include("community:flamingock-mongodb-reactive-auditstore") +project(":community:flamingock-mongodb-reactive-auditstore").name = "flamingock-mongodb-reactive-auditstore" +project(":community:flamingock-mongodb-reactive-auditstore").projectDir = + file("community/flamingock-mongodb-reactive-auditstore") + include("community:flamingock-couchbase-auditstore") project(":community:flamingock-couchbase-auditstore").name = "flamingock-couchbase-auditstore" project(":community:flamingock-couchbase-auditstore").projectDir = file("community/flamingock-couchbase-auditstore") @@ -93,10 +98,16 @@ include("core:target-systems:flamingock-nontransactional-targetsystem") include("core:target-systems:flamingock-mongodb-externalsystem-api") +include("core:target-systems:flamingock-mongodb-reactive-externalsystem-api") + include("core:target-systems:flamingock-mongodb-sync-targetsystem") +include("core:target-systems:flamingock-mongodb-reactive-targetsystem") + include("core:target-systems:flamingock-mongodb-springdata-targetsystem") +include("core:target-systems:flamingock-mongodb-springdata-reactive-targetsystem") + include("core:target-systems:flamingock-sql-externalsystem-api") include("core:target-systems:flamingock-sql-targetsystem") @@ -122,10 +133,25 @@ include("utils:mongodb-util") project(":utils:mongodb-util").name = "mongodb-util" project(":utils:mongodb-util").projectDir = file("utils/mongodb-util") +include("utils:mongodb-sync-util") +project(":utils:mongodb-sync-util").name = "mongodb-sync-util" +project(":utils:mongodb-sync-util").projectDir = file("utils/mongodb-sync-util") + +include("utils:mongodb-reactive-util") +project(":utils:mongodb-reactive-util").name = "mongodb-reactive-util" +project(":utils:mongodb-reactive-util").projectDir = file("utils/mongodb-reactive-util") + +include("utils:flamingock-reactive-util") +project(":utils:flamingock-reactive-util").projectDir = file("utils/reactive-util") + include("utils:mongodb-test-kit") project(":utils:mongodb-test-kit").name = "mongodb-test-kit" project(":utils:mongodb-test-kit").projectDir = file("utils/mongodb-test-kit") +include("utils:mongodb-reactive-test-kit") +project(":utils:mongodb-reactive-test-kit").name = "mongodb-reactive-test-kit" +project(":utils:mongodb-reactive-test-kit").projectDir = file("utils/mongodb-reactive-test-kit") + include("utils:dynamodb-util") project(":utils:dynamodb-util").name = "dynamodb-util" project(":utils:dynamodb-util").projectDir = file("utils/dynamodb-util") diff --git a/utils/mongodb-reactive-test-kit/build.gradle.kts b/utils/mongodb-reactive-test-kit/build.gradle.kts new file mode 100644 index 000000000..8433860d2 --- /dev/null +++ b/utils/mongodb-reactive-test-kit/build.gradle.kts @@ -0,0 +1,20 @@ +dependencies { + implementation(project(":core:flamingock-core")) + implementation(project(":utils:mongodb-util")) + implementation(project(":utils:flamingock-reactive-util")) + implementation(project(":utils:test-util")) + + compileOnly("org.mongodb:mongodb-driver-reactivestreams:4.0.0") +} + +description = "MongoDB reactive streams TestKit for Flamingock testing" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} + +configurations.testImplementation { + extendsFrom(configurations.compileOnly.get()) +} diff --git a/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveAuditStorage.java b/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveAuditStorage.java new file mode 100644 index 000000000..f889e59b5 --- /dev/null +++ b/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveAuditStorage.java @@ -0,0 +1,93 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.mongodb.reactive.kit; + +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.core.kit.audit.AuditStorage; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.mongodb.MongoDBAuditMapper; +import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper; +import io.flamingock.internal.util.constants.CommunityPersistenceConstants; +import io.flamingock.reactive.util.PublisherSync; +import org.bson.Document; + +import java.util.List; +import java.util.stream.Collectors; + +import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_CHANGE_ID; +import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_STATE; + +/** + * MongoDB reactive implementation of AuditStorage for real database testing. + * Only depends on MongoDB client/database and core Flamingock classes. + * Does not depend on MongoDB-specific Flamingock components like MongoDBReactiveTargetSystem. + */ +public class MongoDBReactiveAuditStorage implements AuditStorage { + + private static final String AUDIT_COLLECTION_NAME = CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME; + + private final MongoCollection auditCollection; + private final MongoDBAuditMapper mapper; + + public MongoDBReactiveAuditStorage(MongoDatabase database) { + this(database, AUDIT_COLLECTION_NAME); + } + + public MongoDBReactiveAuditStorage(MongoDatabase database, String collectionName) { + this.auditCollection = database.getCollection(collectionName); + this.mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document())); + } + + @Override + public void addAuditEntry(AuditEntry auditEntry) { + MongoDBDocumentHelper documentHelper = mapper.toDocument(auditEntry); + PublisherSync.complete(auditCollection.insertOne(documentHelper.getDocument())); + } + + @Override + public List getAuditEntries() { + return PublisherSync.collect(auditCollection.find()) + .stream() + .map(doc -> mapper.fromDocument(new MongoDBDocumentHelper(doc))) + .collect(Collectors.toList()); + } + + @Override + public List getAuditEntriesForChange(String changeId) { + return PublisherSync.collect(auditCollection.find(new Document(KEY_CHANGE_ID, changeId))) + .stream() + .map(doc -> mapper.fromDocument(new MongoDBDocumentHelper(doc))) + .collect(Collectors.toList()); + } + + @Override + public long countAuditEntriesWithStatus(AuditEntry.Status status) { + Long count = PublisherSync.first(auditCollection.countDocuments(new Document(KEY_STATE, status.toString()))); + return count != null ? count : 0L; + } + + @Override + public boolean hasAuditEntries() { + Long count = PublisherSync.first(auditCollection.countDocuments()); + return count != null && count > 0; + } + + @Override + public void clear() { + PublisherSync.complete(auditCollection.deleteMany(new Document())); + } +} diff --git a/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveLockStorage.java b/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveLockStorage.java new file mode 100644 index 000000000..b4bd699ef --- /dev/null +++ b/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveLockStorage.java @@ -0,0 +1,135 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.mongodb.reactive.kit; + +import com.mongodb.client.model.ReplaceOptions; +import com.mongodb.reactivestreams.client.MongoCollection; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.core.kit.lock.LockStorage; +import io.flamingock.internal.core.external.store.lock.LockAcquisition; +import io.flamingock.internal.core.external.store.lock.LockKey; +import io.flamingock.internal.util.id.RunnerId; +import io.flamingock.reactive.util.PublisherSync; +import org.bson.Document; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_LOCK_STORE_NAME; + +/** + * MongoDB reactive implementation of LockStorage for real database testing. + * Only depends on MongoDB client/database and core Flamingock classes. + * Does not depend on MongoDB-specific Flamingock components like MongoDBReactiveTargetSystem. + */ +public class MongoDBReactiveLockStorage implements LockStorage { + + private final MongoCollection lockCollection; + private final MongoCollection metadataCollection; + + public MongoDBReactiveLockStorage(MongoDatabase database) { + this(database, DEFAULT_LOCK_STORE_NAME); + } + + public MongoDBReactiveLockStorage(MongoDatabase database, String lockCollectionName) { + this.lockCollection = database.getCollection(lockCollectionName); + this.metadataCollection = database.getCollection(lockCollectionName + "Metadata"); + } + + @Override + public void storeLock(LockKey key, LockAcquisition acquisition) { + Document lockDoc = new Document() + .append("_id", key.toString()) + .append("key", key.toString()) + .append("owner", acquisition.getOwner().getKey()) + .append("leaseMillis", acquisition.getAcquiredForMillis()) + .append("createdAt", new Date()); + + PublisherSync.complete(lockCollection.replaceOne( + new Document("_id", key.toString()), + lockDoc, + new ReplaceOptions().upsert(true) + )); + } + + @Override + public LockAcquisition getLockInfo(LockKey key) { + Document doc = PublisherSync.first(lockCollection.find(new Document("_id", key.toString())).first()); + if (doc == null) { + return null; + } + + return documentToLockAcquisition(doc); + } + + @Override + public Map getAllLocks() { + Map locks = new HashMap<>(); + + for (Document doc : PublisherSync.collect(lockCollection.find())) { + LockKey key = LockKey.fromString(doc.getString("key")); + LockAcquisition acquisition = documentToLockAcquisition(doc); + locks.put(key, acquisition); + } + + return locks; + } + + @Override + public void removeLock(LockKey key) { + PublisherSync.complete(lockCollection.deleteOne(new Document("_id", key.toString()))); + } + + @Override + public boolean hasLocks() { + Long count = PublisherSync.first(lockCollection.countDocuments()); + return count != null && count > 0; + } + + @Override + public void clear() { + PublisherSync.complete(lockCollection.deleteMany(new Document())); + PublisherSync.complete(metadataCollection.deleteMany(new Document())); + } + + @Override + public void setMetadata(String key, Object value) { + Document metadataDoc = new Document() + .append("_id", key) + .append("key", key) + .append("value", value); + + PublisherSync.complete(metadataCollection.replaceOne( + new Document("_id", key), + metadataDoc, + new ReplaceOptions().upsert(true) + )); + } + + @Override + public Object getMetadata(String key) { + Document doc = PublisherSync.first(metadataCollection.find(new Document("_id", key)).first()); + return doc != null ? doc.get("value") : null; + } + + private LockAcquisition documentToLockAcquisition(Document doc) { + RunnerId owner = RunnerId.fromString(doc.getString("owner")); + long leaseMillis = doc.getLong("leaseMillis"); + + return new LockAcquisition(owner, leaseMillis); + } +} diff --git a/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveTestKit.java b/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveTestKit.java new file mode 100644 index 000000000..b494d8fea --- /dev/null +++ b/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveTestKit.java @@ -0,0 +1,58 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.mongodb.reactive.kit; + +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.core.kit.AbstractTestKit; +import io.flamingock.core.kit.audit.AuditStorage; +import io.flamingock.core.kit.lock.LockStorage; +import io.flamingock.internal.core.external.store.CommunityAuditStore; +import io.flamingock.reactive.util.PublisherSync; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class MongoDBReactiveTestKit extends AbstractTestKit { + private static final Set SYSTEM_DATABASES = + new HashSet<>(Arrays.asList("admin", "config", "local")); + + private final MongoClient mongoClient; + + public MongoDBReactiveTestKit(AuditStorage auditStorage, LockStorage lockStorage, CommunityAuditStore auditStore, MongoClient mongoClient) { + super(auditStorage, lockStorage, auditStore); + this.mongoClient = mongoClient; + } + + @Override + public void cleanUp() { + PublisherSync.collect(mongoClient.listDatabaseNames()).forEach(dbName -> { + if (!SYSTEM_DATABASES.contains(dbName)) { + PublisherSync.complete(mongoClient.getDatabase(dbName).drop()); + } + }); + } + + /** + * Create a new MongoDBReactiveTestKit with MongoDB client and database. + */ + public static MongoDBReactiveTestKit create(CommunityAuditStore auditStore, MongoClient mongoClient, MongoDatabase database) { + MongoDBReactiveAuditStorage auditStorage = new MongoDBReactiveAuditStorage(database); + MongoDBReactiveLockStorage lockStorage = new MongoDBReactiveLockStorage(database); + return new MongoDBReactiveTestKit(auditStorage, lockStorage, auditStore, mongoClient); + } +} diff --git a/utils/mongodb-reactive-util/build.gradle.kts b/utils/mongodb-reactive-util/build.gradle.kts new file mode 100644 index 000000000..1e742461d --- /dev/null +++ b/utils/mongodb-reactive-util/build.gradle.kts @@ -0,0 +1,18 @@ +dependencies { + implementation(project(":utils:mongodb-util")) + implementation(project(":core:flamingock-core-commons")) + + compileOnly("org.mongodb:mongodb-driver-reactivestreams:4.0.0") +} + +description = "MongoDB reactive streams driver utilities" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} + +configurations.testImplementation { + extendsFrom(configurations.compileOnly.get()) +} diff --git a/utils/mongodb-reactive-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBReactiveCollectionHelper.java b/utils/mongodb-reactive-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBReactiveCollectionHelper.java new file mode 100644 index 000000000..a16284cc0 --- /dev/null +++ b/utils/mongodb-reactive-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBReactiveCollectionHelper.java @@ -0,0 +1,118 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.common.mongodb; + +import com.mongodb.client.model.IndexOptions; +import com.mongodb.reactivestreams.client.MongoCollection; +import io.flamingock.internal.common.core.error.FlamingockException; +import org.bson.Document; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.util.List; +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +public class MongoDBReactiveCollectionHelper implements CollectionHelper { + + private final MongoCollection collection; + + public MongoDBReactiveCollectionHelper(MongoCollection collection) { + this.collection = collection; + } + + @Override + public String getCollectionName() { + return collection.getNamespace().getCollectionName(); + } + + @Override + public Iterable listIndexes() { + List indexes = collect(collection.listIndexes()); + return indexes.stream().map(MongoDBDocumentHelper::new).collect(Collectors.toList()); + } + + @Override + public String createUniqueIndex(MongoDBDocumentHelper uniqueIndexDocument) { + return first(collection.createIndex( + uniqueIndexDocument.getDocument(), + new IndexOptions().unique(true))); + } + + @Override + public void dropIndex(String indexName) { + complete(collection.dropIndex(indexName)); + } + + @Override + public void deleteMany(MongoDBDocumentHelper documentWrapper) { + first(collection.deleteMany(documentWrapper.getDocument())); + } + + private static T first(Publisher publisher) { + List values = collect(publisher); + return values.isEmpty() ? null : values.get(0); + } + + private static List collect(Publisher publisher) { + List values = new ArrayList<>(); + AtomicReference error = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + publisher.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T value) { + values.add(value); + } + + @Override + public void onError(Throwable throwable) { + error.set(throwable); + latch.countDown(); + } + + @Override + public void onComplete() { + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new FlamingockException(e); + } + Throwable throwable = error.get(); + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } + if (throwable != null) { + throw new FlamingockException(throwable); + } + return values; + } + + private static void complete(Publisher publisher) { + collect(publisher); + } +} diff --git a/utils/mongodb-sync-util/build.gradle.kts b/utils/mongodb-sync-util/build.gradle.kts new file mode 100644 index 000000000..734db3514 --- /dev/null +++ b/utils/mongodb-sync-util/build.gradle.kts @@ -0,0 +1,17 @@ +dependencies { + implementation(project(":utils:mongodb-util")) + + compileOnly("org.mongodb:mongodb-driver-sync:4.0.0") +} + +description = "MongoDB synchronous driver utilities" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} + +configurations.testImplementation { + extendsFrom(configurations.compileOnly.get()) +} diff --git a/utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncCollectionHelper.java b/utils/mongodb-sync-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncCollectionHelper.java similarity index 82% rename from utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncCollectionHelper.java rename to utils/mongodb-sync-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncCollectionHelper.java index 98a36839d..01993b567 100644 --- a/utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncCollectionHelper.java +++ b/utils/mongodb-sync-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncCollectionHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Flamingock (https://www.flamingock.io) + * Copyright 2026 Flamingock (https://www.flamingock.io) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import org.bson.Document; -public class MongoDBSyncCollectionHelper implements CollectionHelper { +public class MongoDBSyncCollectionHelper implements CollectionHelper { private final MongoCollection collection; @@ -36,11 +36,11 @@ public String getCollectionName() { @Override public Iterable listIndexes() { - return collection.listIndexes().map(MongoDBSyncDocumentHelper::new); + return collection.listIndexes().map(MongoDBDocumentHelper::new); } @Override - public String createUniqueIndex(MongoDBSyncDocumentHelper uniqueIndexDocument) { + public String createUniqueIndex(MongoDBDocumentHelper uniqueIndexDocument) { return collection.createIndex(uniqueIndexDocument.getDocument(), new IndexOptions().unique(true)); } @@ -50,7 +50,7 @@ public void dropIndex(String indexName) { } @Override - public void deleteMany(MongoDBSyncDocumentHelper documentWrapper) { + public void deleteMany(MongoDBDocumentHelper documentWrapper) { collection.deleteMany(documentWrapper.getDocument()); } diff --git a/utils/mongodb-util/build.gradle.kts b/utils/mongodb-util/build.gradle.kts index dc40279d3..7e860f249 100644 --- a/utils/mongodb-util/build.gradle.kts +++ b/utils/mongodb-util/build.gradle.kts @@ -1,7 +1,7 @@ dependencies { implementation(project(":core:flamingock-core")) - compileOnly("org.mongodb:mongodb-driver-sync:4.0.0") + compileOnly("org.mongodb:bson:4.0.0") testImplementation(project(":utils:test-util")) } @@ -16,4 +16,4 @@ java { toolchain { languageVersion.set(JavaLanguageVersion.of(8)) } -} \ No newline at end of file +} diff --git a/utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncDocumentHelper.java b/utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBDocumentHelper.java similarity index 86% rename from utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncDocumentHelper.java rename to utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBDocumentHelper.java index b71092771..dd912fd94 100644 --- a/utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncDocumentHelper.java +++ b/utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBDocumentHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Flamingock (https://www.flamingock.io) + * Copyright 2026 Flamingock (https://www.flamingock.io) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,11 @@ import org.bson.Document; -public class MongoDBSyncDocumentHelper implements DocumentHelper { +public class MongoDBDocumentHelper implements DocumentHelper { private final Document document; - public MongoDBSyncDocumentHelper(Document document) { + public MongoDBDocumentHelper(Document document) { this.document = document; } @@ -66,6 +66,6 @@ public int size() { @Override public DocumentHelper getWithWrapper(String key) { - return new MongoDBSyncDocumentHelper((Document) get(key)); + return new MongoDBDocumentHelper((Document) get(key)); } } diff --git a/utils/reactive-util/build.gradle.kts b/utils/reactive-util/build.gradle.kts new file mode 100644 index 000000000..894737f94 --- /dev/null +++ b/utils/reactive-util/build.gradle.kts @@ -0,0 +1,18 @@ +dependencies { + compileOnly("org.reactivestreams:reactive-streams:1.0.3") + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2") +} + +description = "Reactive streams utilities for Flamingock" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} + +configurations.testImplementation { + extendsFrom(configurations.compileOnly.get()) +} diff --git a/utils/reactive-util/src/main/java/io/flamingock/reactive/util/PublisherSync.java b/utils/reactive-util/src/main/java/io/flamingock/reactive/util/PublisherSync.java new file mode 100644 index 000000000..83310bfd4 --- /dev/null +++ b/utils/reactive-util/src/main/java/io/flamingock/reactive/util/PublisherSync.java @@ -0,0 +1,95 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.reactive.util; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +public final class PublisherSync { + + private PublisherSync() { + } + + public static T first(Publisher publisher) { + List values = collect(publisher); + return values.isEmpty() ? null : values.get(0); + } + + public static List collect(Publisher publisher) { + List values = new ArrayList<>(); + await(publisher, values::add); + return values; + } + + public static void complete(Publisher publisher) { + await(publisher, value -> { + }); + } + + private static void await(Publisher publisher, ItemConsumer consumer) { + AtomicReference error = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + publisher.subscribe(new Subscriber() { + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(T value) { + consumer.accept(value); + } + + @Override + public void onError(Throwable throwable) { + error.set(throwable); + latch.countDown(); + } + + @Override + public void onComplete() { + latch.countDown(); + } + }); + await(latch); + Throwable throwable = error.get(); + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } + if (throwable != null) { + throw new PublisherSyncException(throwable); + } + } + + private static void await(CountDownLatch latch) { + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new PublisherSyncException(e); + } + } + + private interface ItemConsumer { + void accept(T value); + } +} diff --git a/utils/reactive-util/src/main/java/io/flamingock/reactive/util/PublisherSyncException.java b/utils/reactive-util/src/main/java/io/flamingock/reactive/util/PublisherSyncException.java new file mode 100644 index 000000000..74e0cd483 --- /dev/null +++ b/utils/reactive-util/src/main/java/io/flamingock/reactive/util/PublisherSyncException.java @@ -0,0 +1,23 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.reactive.util; + +public class PublisherSyncException extends RuntimeException { + + public PublisherSyncException(Throwable cause) { + super(cause); + } +} diff --git a/utils/reactive-util/src/test/java/io/flamingock/reactive/util/PublisherSyncTest.java b/utils/reactive-util/src/test/java/io/flamingock/reactive/util/PublisherSyncTest.java new file mode 100644 index 000000000..9e495a546 --- /dev/null +++ b/utils/reactive-util/src/test/java/io/flamingock/reactive/util/PublisherSyncTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.reactive.util; + +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; + +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PublisherSyncTest { + + @Test + void collectsPublisherValuesInOrder() { + assertEquals(Arrays.asList("first", "second"), PublisherSync.collect(values("first", "second"))); + } + + @Test + void returnsFirstPublisherValue() { + assertEquals("first", PublisherSync.first(values("first", "second"))); + } + + @Test + void returnsNullWhenPublisherIsEmpty() { + assertNull(PublisherSync.first(values())); + } + + @Test + void completesPublisher() { + PublisherSync.complete(values("first", "second")); + } + + @Test + void propagatesRuntimePublisherFailure() { + IllegalStateException failure = new IllegalStateException("failed"); + IllegalStateException result = assertThrows( + IllegalStateException.class, + () -> PublisherSync.collect(error(failure))); + assertSame(failure, result); + } + + @Test + void wrapsCheckedPublisherFailure() { + IOException failure = new IOException("failed"); + PublisherSyncException result = assertThrows( + PublisherSyncException.class, + () -> PublisherSync.collect(error(failure))); + assertSame(failure, result.getCause()); + } + + @Test + void restoresInterruptedFlagWhenInterrupted() throws Exception { + AtomicReference failure = new AtomicReference<>(); + AtomicReference interrupted = new AtomicReference<>(false); + Thread thread = new Thread(() -> { + try { + PublisherSync.complete(neverCompletes()); + } catch (Throwable throwable) { + interrupted.set(Thread.currentThread().isInterrupted()); + failure.set(throwable); + } + }); + + thread.start(); + while (thread.getState() != Thread.State.WAITING) { + Thread.yield(); + } + thread.interrupt(); + thread.join(1000); + + assertFalse(thread.isAlive()); + assertTrue(interrupted.get()); + assertTrue(failure.get() instanceof PublisherSyncException); + } + + @SafeVarargs + private static Publisher values(T... values) { + return subscriber -> subscriber.onSubscribe(new Subscription() { + @Override + public void request(long count) { + for (T value : values) { + subscriber.onNext(value); + } + subscriber.onComplete(); + } + + @Override + public void cancel() { + } + }); + } + + private static Publisher error(Throwable failure) { + return subscriber -> subscriber.onSubscribe(new Subscription() { + @Override + public void request(long count) { + subscriber.onError(failure); + } + + @Override + public void cancel() { + } + }); + } + + private static Publisher neverCompletes() { + return subscriber -> subscriber.onSubscribe(new Subscription() { + @Override + public void request(long count) { + } + + @Override + public void cancel() { + } + }); + } +}