From d3e2fc04f71f469f7bd098f8184f78f6ee6bf7a4 Mon Sep 17 00:00:00 2001 From: davidfrigolet Date: Wed, 22 Jul 2026 14:53:41 +0100 Subject: [PATCH 1/3] feat(mongodb): add mongocksupport to mongock reactive --- .../build.gradle.kts | 1 + .../reactive/MongoDBReactiveTargetSystem.java | 19 +- .../build.gradle.kts | 1 + ...MongoDBSpringDataReactiveTargetSystem.java | 19 +- .../build.gradle.kts | 45 +++++ .../mongodb/reactive/MongockAuditEntry.java | 173 ++++++++++++++++++ .../MongockImporterMongoDBReactive.java | 119 ++++++++++++ .../MongockImporterMongoDBReactiveTest.java | 141 ++++++++++++++ settings.gradle.kts | 4 + 9 files changed, 520 insertions(+), 2 deletions(-) create mode 100644 legacy/mongock-importer-mongodb-reactive/build.gradle.kts create mode 100644 legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockAuditEntry.java create mode 100644 legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactive.java create mode 100644 legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactiveTest.java diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/build.gradle.kts b/core/target-systems/flamingock-mongodb-reactive-targetsystem/build.gradle.kts index ad3dbb813..e95ae9763 100644 --- a/core/target-systems/flamingock-mongodb-reactive-targetsystem/build.gradle.kts +++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/build.gradle.kts @@ -2,6 +2,7 @@ dependencies { api(project(":core:flamingock-core")) implementation(project(":utils:mongodb-util")) implementation(project(":utils:mongodb-reactive-util")) + implementation(project(":legacy:mongock-importer-mongodb-reactive")) api(project(":utils:flamingock-reactive-util")) api(project(":core:target-systems:flamingock-mongodb-reactive-externalsystem-api")) 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 index 6253a34fa..448aa32c9 100644 --- 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 @@ -22,6 +22,7 @@ import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoDatabase; import io.flamingock.externalsystem.mongodb.reactive.api.MongoDBReactiveExternalSystem; +import io.flamingock.importer.mongock.mongodb.reactive.MongockImporterMongoDBReactive; import io.flamingock.internal.common.core.audit.AuditHistoryReader; import io.flamingock.internal.common.core.audit.AuditReaderType; import io.flamingock.internal.common.core.context.ContextResolver; @@ -33,8 +34,12 @@ import io.flamingock.internal.core.transaction.TransactionManager; import io.flamingock.reactive.util.PublisherSync; +import java.util.Objects; import java.util.Optional; +import static io.flamingock.internal.common.core.audit.AuditReaderType.MONGOCK; +import static io.flamingock.internal.common.core.metadata.Constants.DEFAULT_MONGOCK_ORIGIN; +import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY; import static io.flamingock.internal.core.builder.FlamingockEdition.COMMUNITY; public class MongoDBReactiveTargetSystem extends TransactionalTargetSystem @@ -47,6 +52,7 @@ public class MongoDBReactiveTargetSystem extends TransactionalTargetSystem getTxManager() { @Override public void initialize(ContextResolver baseContext) { + this.baseContext = baseContext; this.validate(); targetSystemContext.addDependency(mongoClient); database = mongoClient.getDatabase(databaseName) @@ -150,6 +157,16 @@ public TransactionWrapper getTxWrapper() { @Override public Optional getAuditAuditReader(AuditReaderType type) { - return Optional.empty(); + if (Objects.requireNonNull(type) == MONGOCK) { + return Optional.of(new MongockImporterMongoDBReactive(database, getMongockOrigin())); + } else { + return Optional.empty(); + } + } + + private String getMongockOrigin() { + return targetSystemContext.getProperty(MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY) + .orElse(baseContext.getProperty(MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY) + .orElse(DEFAULT_MONGOCK_ORIGIN)); } } 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 index 298cde8af..b0ca96ccc 100644 --- a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/build.gradle.kts +++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/build.gradle.kts @@ -9,6 +9,7 @@ dependencies { api(project(":core:flamingock-core")) api(project(":utils:flamingock-reactive-util")) api(project(":core:target-systems:flamingock-mongodb-reactive-externalsystem-api")) + implementation(project(":legacy:mongock-importer-mongodb-reactive")) compileOnly("org.mongodb:mongodb-driver-reactivestreams:${versions["mongodb"]}") compileOnly("org.springframework.data:spring-data-mongodb:${versions["spring-data"]}") 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 index 39e42c720..ee3cad453 100644 --- 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 @@ -20,6 +20,7 @@ import com.mongodb.WriteConcern; import com.mongodb.reactivestreams.client.MongoDatabase; import io.flamingock.externalsystem.mongodb.reactive.api.MongoDBReactiveExternalSystem; +import io.flamingock.importer.mongock.mongodb.reactive.MongockImporterMongoDBReactive; import io.flamingock.internal.common.core.audit.AuditHistoryReader; import io.flamingock.internal.common.core.audit.AuditReaderType; import io.flamingock.internal.common.core.context.ContextResolver; @@ -30,8 +31,12 @@ import io.flamingock.internal.core.external.targets.mark.NoOpTargetSystemAuditMarker; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import java.util.Objects; import java.util.Optional; +import static io.flamingock.internal.common.core.audit.AuditReaderType.MONGOCK; +import static io.flamingock.internal.common.core.metadata.Constants.DEFAULT_MONGOCK_ORIGIN; +import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY; import static io.flamingock.internal.core.builder.FlamingockEdition.COMMUNITY; public class MongoDBSpringDataReactiveTargetSystem @@ -44,6 +49,7 @@ public class MongoDBSpringDataReactiveTargetSystem private ReadPreference readPreference = ReadPreference.primary(); private MongoDBSpringDataReactiveTxWrapper txWrapper; + private ContextResolver baseContext; public MongoDBSpringDataReactiveTargetSystem(String id, ReactiveMongoTemplate mongoTemplate) { super(id); @@ -91,6 +97,7 @@ public MongoDatabase getMongoDatabase() { @Override public void initialize(ContextResolver baseContext) { + this.baseContext = baseContext; this.validate(); targetSystemContext.addDependency(mongoTemplate); @@ -134,6 +141,16 @@ public TransactionWrapper getTxWrapper() { @Override public Optional getAuditAuditReader(AuditReaderType type) { - return Optional.empty(); + if (Objects.requireNonNull(type) == MONGOCK) { + return Optional.of(new MongockImporterMongoDBReactive(getMongoDatabase(), getMongockOrigin())); + } else { + return Optional.empty(); + } + } + + private String getMongockOrigin() { + return targetSystemContext.getProperty(MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY) + .orElse(baseContext.getProperty(MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY) + .orElse(DEFAULT_MONGOCK_ORIGIN)); } } diff --git a/legacy/mongock-importer-mongodb-reactive/build.gradle.kts b/legacy/mongock-importer-mongodb-reactive/build.gradle.kts new file mode 100644 index 000000000..56147cee5 --- /dev/null +++ b/legacy/mongock-importer-mongodb-reactive/build.gradle.kts @@ -0,0 +1,45 @@ +dependencies { + implementation(project(":core:flamingock-core-commons")) + implementation(project(":utils:flamingock-reactive-util")) + compileOnly("org.mongodb:mongodb-driver-reactivestreams:4.0.0") + + testAnnotationProcessor(project(":core:flamingock-processor")) + testAnnotationProcessor(project(":legacy:mongock-support")) + testImplementation(project(":legacy:mongock-support")) + testImplementation(project(":core:target-systems:flamingock-mongodb-reactive-targetsystem")) + testImplementation(project(":community:flamingock-mongodb-reactive-auditstore")) + testImplementation(project(":utils:test-util")) + testImplementation(project(":utils:mongodb-reactive-test-kit")) + + 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") + +} + +description = "A MongoDB migration utility that imports Mongock’s execution history into Flamingock-Community’s audit store for smooth project upgrades — reactive driver variant." + + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(8)) + } +} + +tasks.test { + useJUnitPlatform() +} + + +tasks.withType().configureEach { + if (name.contains("Test", ignoreCase = true)) { + options.compilerArgs.addAll(listOf( + "-Asources=${projectDir}/src/test/java", + "-Aresources=${projectDir}/src/test/resources" + )) + } +} +configurations.testImplementation { + extendsFrom(configurations.compileOnly.get()) +} diff --git a/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockAuditEntry.java b/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockAuditEntry.java new file mode 100644 index 000000000..f7b372dbd --- /dev/null +++ b/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockAuditEntry.java @@ -0,0 +1,173 @@ +/* + * 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.importer.mongock.mongodb.reactive; + +import io.flamingock.internal.common.core.audit.AuditEntry; + +import java.util.Date; + +/** + * Mirrors io.flamingock.importer.mongock.mongodb.MongockAuditEntry (sync importer module). + * Duplicated rather than shared to avoid a reactive-importer to sync-importer module + * dependency; this is a plain driver-agnostic POJO with no logic asymmetry risk. + */ +public class MongockAuditEntry { + + protected String executionId; + protected String changeId; + protected String author; + protected Date timestamp; + protected MongockChangeState state; + protected MongockChangeType type; + protected String changeLogClass; + protected String changeSetMethod; + protected Object metadata; + protected long executionMillis; + protected String executionHostname; + protected String errorTrace; + protected Boolean systemChange; + protected Date originalTimestamp; + + + public MongockAuditEntry(String executionId, + String changeId, + String author, + Date timestamp, + String state, + String type, + String changeLogClass, + String changeSetMethod, + Object metadata, + long executionMillis, + String executionHostname, + String errorTrace, + Boolean systemChange, + Date originalTimestamp) { + this.executionId = executionId; + this.changeId = changeId; + this.author = author; + this.timestamp = timestamp; + this.state = MongockAuditEntry.MongockChangeState.valueOf(state); + this.type = parseType(type); + this.changeLogClass = changeLogClass; + this.changeSetMethod = changeSetMethod; + this.metadata = metadata; + this.executionMillis = executionMillis; + this.executionHostname = executionHostname; + this.errorTrace = errorTrace; + this.systemChange = normalizeSystemChange(systemChange); + this.originalTimestamp = originalTimestamp; + } + + public String getExecutionId() { + return executionId; + } + + public String getChangeId() { + return changeId; + } + + public String getAuthor() { + return author; + } + + public Date getTimestamp() { + return timestamp; + } + + public AuditEntry.Status getState() { + return state.toAuditStatus(); + } + + public AuditEntry.ChangeType getType() { + return type.toAuditType(); + } + + public String getChangeLogClass() { + return changeLogClass; + } + + public String getChangeSetMethod() { + return changeSetMethod; + } + + public Object getMetadata() { + return metadata; + } + + public long getExecutionMillis() { + return executionMillis; + } + + public String getExecutionHostname() { + return executionHostname; + } + + public String getErrorTrace() { + return errorTrace; + } + + public Boolean getSystemChange() { + return systemChange; + } + + public Date getOriginalTimestamp() { + return originalTimestamp; + } + + public boolean shouldBeIgnored() { + return state == MongockAuditEntry.MongockChangeState.IGNORED; + } + + private static MongockChangeType parseType(String type) { + if (type == null || type.trim().isEmpty()) { + return MongockChangeType.EXECUTION; + } + return MongockChangeType.valueOf(type); + } + + private static Boolean normalizeSystemChange(Boolean systemChange) { + return systemChange != null ? systemChange : Boolean.FALSE; + } + + + public enum MongockChangeState { + EXECUTED, FAILED, ROLLED_BACK, ROLLBACK_FAILED, IGNORED; + + public AuditEntry.Status toAuditStatus() { + switch (this) { + case FAILED: return AuditEntry.Status.FAILED; + case ROLLED_BACK: return AuditEntry.Status.ROLLED_BACK; + case ROLLBACK_FAILED: return AuditEntry.Status.ROLLBACK_FAILED; + case EXECUTED: + default: return AuditEntry.Status.APPLIED; + } + } + } + + public enum MongockChangeType { + EXECUTION, BEFORE_EXECUTION; + + public AuditEntry.ChangeType toAuditType() { + switch (this) { + case BEFORE_EXECUTION: return AuditEntry.ChangeType.MONGOCK_BEFORE; + case EXECUTION: + default: return AuditEntry.ChangeType.MONGOCK_EXECUTION; + } + } + } + +} diff --git a/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactive.java b/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactive.java new file mode 100644 index 000000000..2f5583445 --- /dev/null +++ b/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactive.java @@ -0,0 +1,119 @@ +/* + * 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.importer.mongock.mongodb.reactive; + +import com.mongodb.reactivestreams.client.MongoCollection; +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.AuditHistoryReader; +import io.flamingock.reactive.util.PublisherSync; +import org.bson.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Mirrors io.flamingock.importer.mongock.mongodb.MongockImporterMongoDB (sync importer), + * reading against the reactive streams driver. AuditHistoryReader#getAuditHistory() is a + * synchronous contract (MongockImportChange calls it directly, no reactive path), so the + * reactive find() is bridged with PublisherSync — same one-shot blocking pattern used + * throughout this branch (target-system database-name resolution, migration change-unit + * saves). + */ +public class MongockImporterMongoDBReactive implements AuditHistoryReader { + + private static final Logger logger = LoggerFactory.getLogger("MongockImporter"); + + private final MongoCollection sourceCollection; + + public MongockImporterMongoDBReactive(MongoDatabase mongoDatabase, String collectionName) { + this.sourceCollection = mongoDatabase.getCollection(collectionName); + } + + @Override + public List getAuditHistory() { + return PublisherSync.collect(sourceCollection.find()) + .stream() + .map(MongockImporterMongoDBReactive::toAuditEntry) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + + private static AuditEntry toAuditEntry(Document document) { + MongockAuditEntry changeEntry = toChangeEntry(document); + LocalDateTime timestamp = Instant.ofEpochMilli(changeEntry.getTimestamp().getTime()) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + + if (changeEntry.shouldBeIgnored()) { + logger.info("Skipping Mongock audit entry with changeId[{}]: state=IGNORED (Mongock never executed this change; nothing to import).", + changeEntry.getChangeId()); + return null; + } + return new AuditEntry( + changeEntry.getExecutionId(), + null, + changeEntry.getChangeId(), + changeEntry.getAuthor(), + timestamp, + changeEntry.getState(), + changeEntry.getType(), + changeEntry.getChangeLogClass(), + changeEntry.getChangeSetMethod(), + null, //TODO: set sourceFile + changeEntry.getExecutionMillis(), + changeEntry.getExecutionHostname(), + changeEntry.getMetadata(), + changeEntry.getSystemChange(), + changeEntry.getErrorTrace(), + null, + null, + null, + RecoveryStrategy.MANUAL_INTERVENTION, + null + ); + } + + + private static MongockAuditEntry toChangeEntry(Document document) { + Date timestamp = document.getDate("timestamp"); + return new MongockAuditEntry( + document.getString("executionId"), + document.getString("changeId"), + document.getString("author"), + timestamp, + document.getString("state"), + document.getString("type"), + document.getString("changeLogClass"), + document.getString("changeSetMethod"), + document.get("metadata"), + document.getLong("executionMillis"), + document.getString("executionHostName"), + document.getString("errorTrace"), + document.getBoolean("systemChange"), + timestamp + ); + } +} diff --git a/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactiveTest.java b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactiveTest.java new file mode 100644 index 000000000..f3feaeaec --- /dev/null +++ b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactiveTest.java @@ -0,0 +1,141 @@ +/* + * 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.importer.mongock.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.internal.common.core.audit.AuditEntry; +import io.flamingock.reactive.util.PublisherSync; +import org.bson.Document; +import org.junit.jupiter.api.AfterAll; +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.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.time.Instant; +import java.util.Date; +import java.util.List; + +/** + * Mirrors io.flamingock.importer.mongock.mongodb.MongoDBImporterTest's core assertions + * (mapping correctness + IGNORED-state filtering) for the reactive driver flavor. Full + * pipeline-level E2E proof (real upgrade via a booted JHipster reactive app with + * @MongockSupport wired to the reactive target system) lives in the JHipster fork's + * poc/gradle-sb4-reactive PoC. + */ +@Testcontainers +class MongockImporterMongoDBReactiveTest { + + private static final String DB_NAME = "importerTest"; + private static final String LEGACY_COLLECTION = "mongockChangeLog"; + + private static MongoDBContainer mongoDBContainer; + private static MongoClient mongoClient; + private static MongoDatabase database; + + @BeforeAll + static void setUpContainer() { + mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:8.2.9")); + mongoDBContainer.start(); + MongoClientSettings settings = MongoClientSettings.builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getReplicaSetUrl())) + .build(); + mongoClient = MongoClients.create(settings); + database = mongoClient.getDatabase(DB_NAME); + } + + @AfterAll + static void tearDownContainer() { + mongoClient.close(); + mongoDBContainer.stop(); + } + + @BeforeEach + void setUp() { + PublisherSync.complete(database.getCollection(LEGACY_COLLECTION).drop()); + } + + @AfterEach + void tearDown() { + PublisherSync.complete(database.getCollection(LEGACY_COLLECTION).drop()); + } + + @Test + @DisplayName("Should map an EXECUTED legacy entry to an AuditEntry") + void shouldMapExecutedEntry() { + seed(document("users-initialization", "EXECUTED", "EXECUTION", "pretend-mongock-run")); + + MongockImporterMongoDBReactive importer = new MongockImporterMongoDBReactive(database, LEGACY_COLLECTION); + List history = importer.getAuditHistory(); + + Assertions.assertEquals(1, history.size()); + AuditEntry entry = history.get(0); + Assertions.assertEquals("users-initialization", entry.getChangeId()); + Assertions.assertEquals(AuditEntry.Status.APPLIED, entry.getState()); + Assertions.assertEquals(AuditEntry.ChangeType.MONGOCK_EXECUTION, entry.getType()); + Assertions.assertEquals("pretend-mongock-run", entry.getExecutionId()); + } + + @Test + @DisplayName("Should skip an IGNORED legacy entry without throwing") + void shouldSkipIgnoredEntry() { + seed(document("users-initialization", "EXECUTED", "EXECUTION", "pretend-mongock-run")); + seed(document("ghost-extra", "IGNORED", "EXECUTION", null)); + + MongockImporterMongoDBReactive importer = new MongockImporterMongoDBReactive(database, LEGACY_COLLECTION); + List history = importer.getAuditHistory(); + + Assertions.assertEquals(1, history.size(), "IGNORED entry must be filtered out, not throw"); + Assertions.assertEquals("users-initialization", history.get(0).getChangeId()); + } + + @Test + @DisplayName("Should return an empty list when the origin collection is empty") + void shouldReturnEmptyListForEmptyOrigin() { + MongockImporterMongoDBReactive importer = new MongockImporterMongoDBReactive(database, LEGACY_COLLECTION); + Assertions.assertTrue(importer.getAuditHistory().isEmpty()); + } + + private void seed(Document document) { + PublisherSync.complete(database.getCollection(LEGACY_COLLECTION).insertOne(document)); + } + + private static Document document(String changeId, String state, String type, String executionId) { + Document doc = new Document() + .append("changeId", changeId) + .append("state", state) + .append("type", type) + .append("author", "jhipster") + .append("changeLogClass", "com.example.config.dbmigrations._0001__InitialSetupMigration") + .append("changeSetMethod", "changeSet") + .append("executionMillis", 12L) + .append("executionHostName", "test") + .append("timestamp", Date.from(Instant.now())); + if (executionId != null) { + doc.append("executionId", executionId); + } + return doc; + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 55f05cff1..f926f3d61 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -184,6 +184,10 @@ include("legacy:mongock-importer-mongodb") project(":legacy:mongock-importer-mongodb").name = "mongock-importer-mongodb" project(":legacy:mongock-importer-mongodb").projectDir = file("legacy/mongock-importer-mongodb") +include("legacy:mongock-importer-mongodb-reactive") +project(":legacy:mongock-importer-mongodb-reactive").name = "mongock-importer-mongodb-reactive" +project(":legacy:mongock-importer-mongodb-reactive").projectDir = file("legacy/mongock-importer-mongodb-reactive") + include("legacy:mongock-importer-dynamodb") project(":legacy:mongock-importer-dynamodb").name = "mongock-importer-dynamodb" project(":legacy:mongock-importer-dynamodb").projectDir = file("legacy/mongock-importer-dynamodb") From 478ee30d5f74cf842329d38fce7ebcb60ef4fcc2 Mon Sep 17 00:00:00 2001 From: Oliver Santana Date: Thu, 23 Jul 2026 19:30:12 +0100 Subject: [PATCH 2/3] feat(mongodb): add mongocksupport to mongock reactive --- .github/workflows/release.yml | 14 + .../flamingock.project-structure.gradle.kts | 1 + .../mongodb/reactive/MongockAuditEntry.java | 61 +- .../MongockImporterMongoDBReactive.java | 16 - .../MongoDBMongockReactiveTestHelper.java | 77 ++ .../MongoDBReactiveImporterE2ETest.java | 763 ++++++++++++++++++ .../MongockImporterMongoDBReactiveTest.java | 14 +- .../reactive/changes/MongockChangeGroup.java | 40 + .../_0003__CreateUsersCollectionChange.java | 33 + .../changes/_0004__SeedUsersChange.java | 40 + 10 files changed, 1028 insertions(+), 31 deletions(-) create mode 100644 legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongoDBMongockReactiveTestHelper.java create mode 100644 legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongoDBReactiveImporterE2ETest.java create mode 100644 legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/changes/MongockChangeGroup.java create mode 100644 legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/changes/_0003__CreateUsersCollectionChange.java create mode 100644 legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/changes/_0004__SeedUsersChange.java diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6fc3843bd..79f89fb20 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -618,6 +618,19 @@ jobs: FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + mongock-importer-mongodb-reactive: + needs: [ build ] + uses: ./.github/workflows/module-release-graalvm.yml + with: + module: mongock-importer-mongodb-reactive + 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 }} + mongock-importer-dynamodb: needs: [ build ] uses: ./.github/workflows/module-release-graalvm.yml @@ -721,6 +734,7 @@ jobs: sql-test-kit, mongock-support, mongock-importer-mongodb, + mongock-importer-mongodb-reactive, mongock-importer-dynamodb, mongock-importer-couchbase, flamingock-test-support, diff --git a/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts b/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts index 6a1973de3..f1ff515b1 100644 --- a/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts +++ b/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts @@ -69,6 +69,7 @@ val utilProjects = setOf( val legacyProjects = setOf( "mongock-support", "mongock-importer-mongodb", + "mongock-importer-mongodb-reactive", "mongock-importer-dynamodb", "mongock-importer-couchbase" ) diff --git a/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockAuditEntry.java b/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockAuditEntry.java index f7b372dbd..2cc4cc4b6 100644 --- a/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockAuditEntry.java +++ b/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockAuditEntry.java @@ -19,11 +19,6 @@ import java.util.Date; -/** - * Mirrors io.flamingock.importer.mongock.mongodb.MongockAuditEntry (sync importer module). - * Duplicated rather than shared to avoid a reactive-importer to sync-importer module - * dependency; this is a plain driver-agnostic POJO with no logic asymmetry risk. - */ public class MongockAuditEntry { protected String executionId; @@ -76,58 +71,114 @@ public String getExecutionId() { return executionId; } + public void setExecutionId(String executionId) { + this.executionId = executionId; + } + public String getChangeId() { return changeId; } + public void setChangeId(String changeId) { + this.changeId = changeId; + } + public String getAuthor() { return author; } + public void setAuthor(String author) { + this.author = author; + } + public Date getTimestamp() { return timestamp; } + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + public AuditEntry.Status getState() { return state.toAuditStatus(); } + public void setState(String state) { + this.state = MongockAuditEntry.MongockChangeState.valueOf(state); + } + public AuditEntry.ChangeType getType() { return type.toAuditType(); } + public void setType(String type) { + this.type = parseType(type); + } + public String getChangeLogClass() { return changeLogClass; } + public void setChangeLogClass(String changeLogClass) { + this.changeLogClass = changeLogClass; + } + public String getChangeSetMethod() { return changeSetMethod; } + public void setChangeSetMethod(String changeSetMethod) { + this.changeSetMethod = changeSetMethod; + } + public Object getMetadata() { return metadata; } + public void setMetadata(Object metadata) { + this.metadata = metadata; + } + public long getExecutionMillis() { return executionMillis; } + public void setExecutionMillis(long executionMillis) { + this.executionMillis = executionMillis; + } + public String getExecutionHostname() { return executionHostname; } + public void setExecutionHostname(String executionHostname) { + this.executionHostname = executionHostname; + } + public String getErrorTrace() { return errorTrace; } + public void setErrorTrace(String errorTrace) { + this.errorTrace = errorTrace; + } + public Boolean getSystemChange() { return systemChange; } + public void setSystemChange(Boolean systemChange) { + this.systemChange = normalizeSystemChange(systemChange); + } + public Date getOriginalTimestamp() { return originalTimestamp; } + public void setOriginalTimestamp(Date originalTimestamp) { + this.originalTimestamp = originalTimestamp; + } + public boolean shouldBeIgnored() { return state == MongockAuditEntry.MongockChangeState.IGNORED; } diff --git a/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactive.java b/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactive.java index 2f5583445..ed7a666d3 100644 --- a/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactive.java +++ b/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactive.java @@ -22,29 +22,16 @@ import io.flamingock.internal.common.core.audit.AuditHistoryReader; import io.flamingock.reactive.util.PublisherSync; import org.bson.Document; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; -/** - * Mirrors io.flamingock.importer.mongock.mongodb.MongockImporterMongoDB (sync importer), - * reading against the reactive streams driver. AuditHistoryReader#getAuditHistory() is a - * synchronous contract (MongockImportChange calls it directly, no reactive path), so the - * reactive find() is bridged with PublisherSync — same one-shot blocking pattern used - * throughout this branch (target-system database-name resolution, migration change-unit - * saves). - */ public class MongockImporterMongoDBReactive implements AuditHistoryReader { - private static final Logger logger = LoggerFactory.getLogger("MongockImporter"); - private final MongoCollection sourceCollection; public MongockImporterMongoDBReactive(MongoDatabase mongoDatabase, String collectionName) { @@ -56,7 +43,6 @@ public List getAuditHistory() { return PublisherSync.collect(sourceCollection.find()) .stream() .map(MongockImporterMongoDBReactive::toAuditEntry) - .filter(Objects::nonNull) .collect(Collectors.toList()); } @@ -68,8 +54,6 @@ private static AuditEntry toAuditEntry(Document document) { .toLocalDateTime(); if (changeEntry.shouldBeIgnored()) { - logger.info("Skipping Mongock audit entry with changeId[{}]: state=IGNORED (Mongock never executed this change; nothing to import).", - changeEntry.getChangeId()); return null; } return new AuditEntry( diff --git a/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongoDBMongockReactiveTestHelper.java b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongoDBMongockReactiveTestHelper.java new file mode 100644 index 000000000..c9e8b731c --- /dev/null +++ b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongoDBMongockReactiveTestHelper.java @@ -0,0 +1,77 @@ +/* + * 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.importer.mongock.mongodb.reactive; + +import com.mongodb.reactivestreams.client.MongoCollection; +import io.flamingock.common.test.mongock.MongockChangeEntry; +import io.flamingock.common.test.mongock.MongockTestHelper; +import io.flamingock.reactive.util.PublisherSync; +import org.bson.Document; + +import java.util.ArrayList; +import java.util.List; + +public class MongoDBMongockReactiveTestHelper implements MongockTestHelper { + + private final MongoCollection changeLogCollection; + + public MongoDBMongockReactiveTestHelper(MongoCollection changeLogCollection) { + this.changeLogCollection = changeLogCollection; + } + + @Override + public void reset() { + PublisherSync.complete(changeLogCollection.drop()); + } + + public void write(MongockChangeEntry entry) { + PublisherSync.complete(changeLogCollection.insertOne(convertToDocument(entry))); + } + + public int writeAll(List entries) { + List documents = new ArrayList<>(entries.size()); + for (MongockChangeEntry entry : entries) { + documents.add(convertToDocument(entry)); + } + PublisherSync.complete(changeLogCollection.insertMany(documents)); + return documents.size(); + } + + private Document convertToDocument(MongockChangeEntry entry) { + Document document = new Document(); + document.put("executionId", entry.getExecutionId()); + document.put("changeId", entry.getChangeId()); + document.put("author", entry.getAuthor()); + document.put("timestamp", entry.getTimestamp()); + putIfNotNull(document, "state", entry.getState() != null ? entry.getState().toString() : null); + putIfNotNull(document, "type", entry.getType() != null ? entry.getType().toString() : null); + putIfNotNull(document, "changeLogClass", entry.getChangeLogClass()); + putIfNotNull(document, "changeSetMethod", entry.getChangeSetMethod()); + putIfNotNull(document, "metadata", entry.getMetadata()); + document.put("executionMillis", entry.getExecutionMillis()); + putIfNotNull(document, "executionHostname", entry.getExecutionHostname()); + putIfNotNull(document, "errorTrace", entry.getErrorTrace()); + putIfNotNull(document, "systemChange", entry.getSystemChange()); + putIfNotNull(document, "originalTimestamp", entry.getOriginalTimestamp()); + return document; + } + + private void putIfNotNull(Document document, String key, Object value) { + if (value != null) { + document.put(key, value); + } + } +} diff --git a/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongoDBReactiveImporterE2ETest.java b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongoDBReactiveImporterE2ETest.java new file mode 100644 index 000000000..a49a88763 --- /dev/null +++ b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongoDBReactiveImporterE2ETest.java @@ -0,0 +1,763 @@ +/* + * 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.importer.mongock.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.annotations.EnableFlamingock; +import io.flamingock.api.annotations.Stage; +import io.flamingock.common.test.mongock.MongockChangeEntry; +import io.flamingock.common.test.mongock.MongockChangeState; +import io.flamingock.common.test.mongock.MongockTestHelper; +import io.flamingock.core.kit.TestKit; +import io.flamingock.core.kit.audit.AuditTestHelper; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.core.response.data.ErrorInfo; +import io.flamingock.internal.core.builder.runner.Runner; +import io.flamingock.internal.core.operation.StagedExecuteOperationException; +import io.flamingock.mongodb.reactive.kit.MongoDBReactiveTestKit; +import io.flamingock.reactive.util.PublisherSync; +import io.flamingock.store.mongodb.reactive.MongoDBReactiveAuditStore; +import io.flamingock.support.mongock.annotations.MongockSupport; +import io.flamingock.targetsystem.mongodb.reactive.MongoDBReactiveTargetSystem; +import org.bson.Document; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +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.ArrayList; +import java.util.List; + +import static io.flamingock.core.kit.audit.AuditEntryExpectation.APPLIED; +import static io.flamingock.core.kit.audit.AuditEntryExpectation.STARTED; +import static io.flamingock.core.kit.audit.AuditEntryExpectation.auditEntry; +import static io.flamingock.internal.common.core.metadata.Constants.DEFAULT_MONGOCK_ORIGIN; +import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY; +import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_IGNORE_UNKNOWN_ENTRIES_PROPERTY_KEY; +import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY; +import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_SKIP_PROPERTY_KEY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Testcontainers +@MongockSupport(targetSystem = "mongodb-target-system") +@EnableFlamingock(stages = {@Stage(location = "io.flamingock.importer.mongock.mongodb.reactive.changes")}) +public class MongoDBReactiveImporterE2ETest { + + @Container + private static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + private static final String DB_NAME = "test"; + private static final String DATABASE_NAME = "test"; + private static MongoClient mongoClient; + private static MongoDatabase database; + private MongoDBMongockReactiveTestHelper mongockTestHelper; + private TestKit testKit; + private AuditTestHelper auditHelper; + + + @BeforeEach + void setUp() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getReplicaSetUrl())) + .build()); + database = mongoClient.getDatabase(DB_NAME); + + mongockTestHelper = new MongoDBMongockReactiveTestHelper(database.getCollection(DEFAULT_MONGOCK_ORIGIN)); + + MongoDBReactiveTargetSystem targetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, DATABASE_NAME); + testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(targetSystem), mongoClient, database); + auditHelper = testKit.getAuditHelper(); + + } + + @AfterEach + void tearDown() { + PublisherSync.complete(database.drop()); + mongoClient.close(); + } + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed" + + "WHEN migrating to Flamingock Community " + + "THEN should import the entire history " + + "AND execute the pending flamingock changes") + void GIVEN_allMongockChangeUnitsAlreadyExecuted_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { + // Setup Mongock entries + mongockTestHelper.setupBasicScenario(); + + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .build(); + + flamingock.run(); + + // Verify audit sequence: 11 total entries as shown in actual execution + // Legacy imports only show APPLIED (imported from Mongock), new changes show STARTED+APPLIED + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("system-change-00001_before"), + APPLIED("system-change-00001"), + APPLIED("mongock-change-1_before"), + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), + + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Application stage - new changes created by templates + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + + + + + + + // Validate actual change + List users = PublisherSync.collect(database.getCollection("users").find()); + + assertEquals(2, users.size()); + Assertions.assertEquals("Admin", users.get(0).getString("name")); + Assertions.assertEquals("admin@company.com", users.get(0).getString("email")); + Assertions.assertEquals("superuser", users.get(0).getList("roles", String.class).get(0)); + + Assertions.assertEquals("Backup", users.get(1).getString("name")); + Assertions.assertEquals("backup@company.com", users.get(1).getString("email")); + Assertions.assertEquals("readonly", users.get(1).getList("roles", String.class).get(0)); + } + + + @Test + @DisplayName("GIVEN some Mongock changeUnits already executed " + + "AND some other Mongock changeUnits pending for execution" + + "WHEN migrating to Flamingock Community" + + "THEN migrates the history with the executed changeUnits " + + "AND executes the pending Mongock changeUnits " + + "AND executes the pending Flamingock changes") + void GIVEN_someChangeUnitsAlreadyExecuted_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { + // Setup Mongock entries + mongockTestHelper.setupWithOnlyOneChange(); + + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .build(); + + flamingock.run(); + + // Verify audit sequence: 11 total entries as shown in actual execution + // Legacy imports only show APPLIED (imported from Mongock), new changes show STARTED+APPLIED + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("system-change-00001_before"), + APPLIED("system-change-00001"), + APPLIED("mongock-change-1_before"), + APPLIED("mongock-change-1"), + + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + STARTED("mongock-change-2"), + APPLIED("mongock-change-2"), + + // Application stage - new changes created by templates + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + + + + + + + // Validate actual change + List users = PublisherSync.collect(database.getCollection("users").find()); + + assertEquals(2, users.size()); + Assertions.assertEquals("Admin", users.get(0).getString("name")); + Assertions.assertEquals("admin@company.com", users.get(0).getString("email")); + Assertions.assertEquals("superuser", users.get(0).getList("roles", String.class).get(0)); + + Assertions.assertEquals("Backup", users.get(1).getString("name")); + Assertions.assertEquals("backup@company.com", users.get(1).getString("email")); + Assertions.assertEquals("readonly", users.get(1).getList("roles", String.class).get(0)); + } + + @Test + @DisplayName("GIVEN mongock audit history empty " + + "AND no fail if empty origin value provided " + + "WHEN migrating to Flamingock Community" + + "THEN should throw exception") + void GIVEN_mongockAuditHistoryEmptyAndNoFailIfEmptyOriginValueProvided_WHEN_migratingToFlamingockCommunity_THEN_shouldThrowException() { + // Setup Mongock entries + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .build(); + + StagedExecuteOperationException ex = assertThrows(StagedExecuteOperationException.class, flamingock::run); + assertEquals("No audit entries found when importing from 'mongodb-target-system'.", + firstFailedStageErrorMessage(ex)); + + } + + @Test + @DisplayName("GIVEN mongock audit history empty " + + "AND explicit empty origin allowed disabled " + + "WHEN migrating to Flamingock Community" + + "THEN should throw exception") + void GIVEN_mongockAuditHistoryEmptyAndFailIfEmptyOriginEnabled_WHEN_migratingToFlamingockCommunity_THEN_shouldThrowException() { + // Setup Mongock entries + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY, Boolean.FALSE.toString()) + .build(); + + StagedExecuteOperationException ex = assertThrows(StagedExecuteOperationException.class, flamingock::run); + assertEquals("No audit entries found when importing from 'mongodb-target-system'.", + firstFailedStageErrorMessage(ex)); + + } + + @Test + @DisplayName("GIVEN mongock audit history empty " + + "AND explicit empty origin allowed enabled " + + "WHEN migrating to Flamingock Community " + + "THEN should execute the pending Mongock changeUnits " + + "AND execute the pending flamingock changes") + void GIVEN_mongockAuditHistoryEmptyAndFailIfEmptyOriginDisabled_WHEN_migratingToFlamingockCommunity_THEN_shouldThrowException() { + // Setup Mongock entries + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY, Boolean.TRUE.toString()) + .build(); + + flamingock.run(); + + // Verify audit sequence: 10 total entries as shown in actual execution + auditHelper.verifyAuditSequenceStrict( + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Legacy changes + STARTED("mongock-change-1"), + APPLIED("mongock-change-1"), + STARTED("mongock-change-2"), + APPLIED("mongock-change-2"), + + // Application stage - new changes created by templates + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + + + // Validate actual change + List users = PublisherSync.collect(database.getCollection("users").find()); + + assertEquals(2, users.size()); + Assertions.assertEquals("Admin", users.get(0).getString("name")); + Assertions.assertEquals("admin@company.com", users.get(0).getString("email")); + Assertions.assertEquals("superuser", users.get(0).getList("roles", String.class).get(0)); + + Assertions.assertEquals("Backup", users.get(1).getString("name")); + Assertions.assertEquals("backup@company.com", users.get(1).getString("email")); + Assertions.assertEquals("readonly", users.get(1).getList("roles", String.class).get(0)); + } + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed" + + "AND custom origin repository name provided by literal value " + + "WHEN migrating to Flamingock Community " + + "THEN should import the entire history " + + "AND execute the pending flamingock changes") + void GIVEN_allMongockChangeUnitsAlreadyExecutedAndCustomOriginProvidedByLiteralValue_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { + // Setup Mongock entries + + final String customMongockOrigin = "mongockCustomOriginCollection"; + + MongoDBMongockReactiveTestHelper customOriginMongockTestHelper = + new MongoDBMongockReactiveTestHelper(database.getCollection(customMongockOrigin)); + customOriginMongockTestHelper.setupBasicScenario(); + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY, customMongockOrigin) + .build(); + + flamingock.run(); + + // Verify audit sequence: 11 total entries as shown in actual execution + // Legacy imports only show APPLIED (imported from Mongock), new changes show STARTED+APPLIED + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("system-change-00001_before"), + APPLIED("system-change-00001"), + APPLIED("mongock-change-1_before"), + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), + + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Application stage - new changes created by templates + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + + + + + // Validate actual change + List users = PublisherSync.collect(database.getCollection("users").find()); + + assertEquals(2, users.size()); + Assertions.assertEquals("Admin", users.get(0).getString("name")); + Assertions.assertEquals("admin@company.com", users.get(0).getString("email")); + Assertions.assertEquals("superuser", users.get(0).getList("roles", String.class).get(0)); + + Assertions.assertEquals("Backup", users.get(1).getString("name")); + Assertions.assertEquals("backup@company.com", users.get(1).getString("email")); + Assertions.assertEquals("readonly", users.get(1).getList("roles", String.class).get(0)); + } + + @Test + @DisplayName("GIVEN Mongock v4 style audit entries without type, errorTrace and systemChange " + + "WHEN migrating to Flamingock Community " + + "THEN should import the history using v4 compatibility defaults") + void GIVEN_mongockV4StyleAuditEntries_WHEN_migratingToFlamingockCommunity_THEN_shouldImportWithCompatibilityDefaults() { + mongockTestHelper.writeAll(buildMongockV4ExecutedEntries()); + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .build(); + + flamingock.run(); + + auditHelper.verifyAuditSequenceStrict( + auditEntry().withChangeId("mongock-change-1") + .withState(AuditEntry.Status.APPLIED) + .withType(AuditEntry.ChangeType.MONGOCK_EXECUTION) + .withSystemChange(false), + auditEntry().withChangeId("mongock-change-2") + .withState(AuditEntry.Status.APPLIED) + .withType(AuditEntry.ChangeType.MONGOCK_EXECUTION) + .withSystemChange(false), + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + + AuditEntry importedChange1 = getAuditEntryByChangeId("mongock-change-1"); + AuditEntry importedChange2 = getAuditEntryByChangeId("mongock-change-2"); + + assertNotNull(importedChange1); + assertEquals(AuditEntry.ChangeType.MONGOCK_EXECUTION, importedChange1.getType()); + assertFalse(importedChange1.getSystemChange()); + assertNull(importedChange1.getErrorTrace()); + + assertNotNull(importedChange2); + assertEquals(AuditEntry.ChangeType.MONGOCK_EXECUTION, importedChange2.getType()); + assertFalse(importedChange2.getSystemChange()); + assertNull(importedChange2.getErrorTrace()); + } + + @Test + @DisplayName("GIVEN Mongock audit history contains unknown entries " + + "AND relaxed import flag is not provided " + + "WHEN migrating to Flamingock Community " + + "THEN should fail with the current strict validation") + void GIVEN_unknownAuditEntriesAndImplicitStrictMode_WHEN_migratingToFlamingockCommunity_THEN_shouldFail() { + mongockTestHelper.setupWithUnknownChange(); + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .build(); + + StagedExecuteOperationException ex = assertThrows(StagedExecuteOperationException.class, flamingock::run); + assertEquals("Error importing audit entry with changeId[foreign-change-1]: no matching change was found in the current Flamingock pipeline.", + firstFailedStageErrorMessage(ex)); + } + + @Test + @DisplayName("GIVEN Mongock audit history contains unknown entries " + + "AND relaxed import flag is explicitly disabled " + + "WHEN migrating to Flamingock Community " + + "THEN should fail with the current strict validation") + void GIVEN_unknownAuditEntriesAndExplicitStrictMode_WHEN_migratingToFlamingockCommunity_THEN_shouldFail() { + mongockTestHelper.setupWithUnknownChange(); + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_IGNORE_UNKNOWN_ENTRIES_PROPERTY_KEY, Boolean.FALSE.toString()) + .build(); + + StagedExecuteOperationException ex = assertThrows(StagedExecuteOperationException.class, flamingock::run); + assertEquals("Error importing audit entry with changeId[foreign-change-1]: no matching change was found in the current Flamingock pipeline.", + firstFailedStageErrorMessage(ex)); + } + + @Test + @DisplayName("GIVEN Mongock audit history contains unknown entries " + + "AND relaxed import flag is enabled " + + "WHEN migrating to Flamingock Community " + + "THEN should skip the unknown entries and continue") + void GIVEN_unknownAuditEntriesAndRelaxedMode_WHEN_migratingToFlamingockCommunity_THEN_shouldSkipUnknownEntries() { + mongockTestHelper.setupWithUnknownChange(); + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_IGNORE_UNKNOWN_ENTRIES_PROPERTY_KEY, Boolean.TRUE.toString()) + .build(); + + flamingock.run(); + + auditHelper.verifyAuditSequenceStrict( + APPLIED("system-change-00001_before"), + APPLIED("system-change-00001"), + APPLIED("mongock-change-1_before"), + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + } + + @Test + @DisplayName("GIVEN relaxed import flag with invalid value " + + "WHEN migrating to Flamingock Community " + + "THEN should throw exception") + void GIVEN_relaxedImportFlagWithInvalidValue_WHEN_migratingToFlamingockCommunity_THEN_shouldThrowException() { + mongockTestHelper.setupWithUnknownChange(); + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + final String flagValue = "invalid_value"; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_IGNORE_UNKNOWN_ENTRIES_PROPERTY_KEY, flagValue) + .build(); + + StagedExecuteOperationException ex = assertThrows(StagedExecuteOperationException.class, flamingock::run); + assertEquals("Invalid value for " + MONGOCK_IMPORT_IGNORE_UNKNOWN_ENTRIES_PROPERTY_KEY + ": " + flagValue + + " (expected \"true\" or \"false\" or empty)", + firstFailedStageErrorMessage(ex)); + } + + @Test + @DisplayName("GIVEN skip import flag with invalid value " + + "WHEN migrating to Flamingock Community" + + "THEN should throw exception") + void GIVEN_skipImportFlagWithInvalidValue_WHEN_migratingToFlamingockCommunity_THEN_shouldThrowException() { + // Setup Mongock entries + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + final String SKIP_IMPORT_VALUE = "invalid_value"; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + StagedExecuteOperationException ex = assertThrows(StagedExecuteOperationException.class, flamingock::run); + assertEquals("Invalid value for " + MONGOCK_IMPORT_SKIP_PROPERTY_KEY + ": " + SKIP_IMPORT_VALUE + + " (expected \"true\" or \"false\" or empty)", firstFailedStageErrorMessage(ex)); + } + + private static String firstFailedStageErrorMessage(StagedExecuteOperationException ex) { + return ex.getResult().getStages().stream() + .filter(s -> s.getState().isFailed()) + .findFirst() + .flatMap(s -> s.getState().getErrorInfo()) + .map(ErrorInfo::getMessage) + .orElseThrow(() -> new AssertionError("Expected a failed stage with ErrorInfo")); + } + + private List buildMongockV4ExecutedEntries() { + try { + List entries = new ArrayList<>(); + entries.add(new MongockChangeEntry( + "v4-execution-1", + "mongock-change-1", + "mongock", + MongockTestHelper.DEFAULT_DATE_FORMAT.parse("2025-06-19T05:43:57.132Z"), + MongockChangeState.EXECUTED, + null, + "io.mongock.examples.mongodb.standalone.mondogb.sync.migration.initializer.ClientInitializerChangeUnit", + "apply", + null, + 23L, + MongockTestHelper.DEFAULT_HOSTNAME, + null, + null, + null + )); + entries.add(new MongockChangeEntry( + "v4-execution-1", + "mongock-change-2", + "mongock", + MongockTestHelper.DEFAULT_DATE_FORMAT.parse("2025-06-19T05:43:57.169Z"), + MongockChangeState.EXECUTED, + null, + "io.mongock.examples.mongodb.standalone.mondogb.sync.migration.updater.ClientUpdaterChangeUnit", + "apply", + null, + 20L, + MongockTestHelper.DEFAULT_HOSTNAME, + null, + null, + null + )); + return entries; + } catch (Exception e) { + throw new RuntimeException("Failed to build Mongock v4 test entries", e); + } + } + + private AuditEntry getAuditEntryByChangeId(String changeId) { + return auditHelper.getAuditEntriesSorted().stream() + .filter(entry -> changeId.equals(entry.getChangeId())) + .findFirst() + .orElse(null); + } + + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed " + + "AND skip import flag enabled " + + "WHEN migrating to Flamingock Community" + + "THEN should not import any audit history entry " + + "AND execute the all mongock and flamingock changes") + void GIVEN_skipImportFlagEnabled_WHEN_migratingToFlamingockCommunity_THEN_shouldNotMigrateAnyAuditLog() { + + // Setup Mongock entries + mongockTestHelper.setupBasicScenario(); + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + final String SKIP_IMPORT_VALUE = "true"; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + flamingock.run(); + + // Verify audit sequence: 10 total entries as shown in actual execution + auditHelper.verifyAuditSequenceStrict( + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Legacy changes + STARTED("mongock-change-1"), + APPLIED("mongock-change-1"), + STARTED("mongock-change-2"), + APPLIED("mongock-change-2"), + + // Application stage - new changes created by templates + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + + + // Validate actual change + List users = PublisherSync.collect(database.getCollection("users").find()); + + assertEquals(2, users.size()); + Assertions.assertEquals("Admin", users.get(0).getString("name")); + Assertions.assertEquals("admin@company.com", users.get(0).getString("email")); + Assertions.assertEquals("superuser", users.get(0).getList("roles", String.class).get(0)); + + Assertions.assertEquals("Backup", users.get(1).getString("name")); + Assertions.assertEquals("backup@company.com", users.get(1).getString("email")); + Assertions.assertEquals("readonly", users.get(1).getList("roles", String.class).get(0)); + } + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed " + + "AND skip import flag disabled (explicit) " + + "WHEN migrating to Flamingock Community " + + "THEN should import the entire history " + + "AND execute the pending flamingock changes") + void GIVEN_allMongockChangeUnitsAlreadyExecutedAndSkipImportFlagDisabledExplicit_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { + + // Setup Mongock entries + mongockTestHelper.setupBasicScenario(); + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + final String SKIP_IMPORT_VALUE = "false"; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + flamingock.run(); + + // Verify audit sequence: 11 total entries as shown in actual execution + // Legacy imports only show APPLIED (imported from Mongock), new changes show STARTED+APPLIED + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("system-change-00001_before"), + APPLIED("system-change-00001"), + APPLIED("mongock-change-1_before"), + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), + + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Application stage - new changes created by templates + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + + + // Validate actual change + List users = PublisherSync.collect(database.getCollection("users").find()); + + assertEquals(2, users.size()); + Assertions.assertEquals("Admin", users.get(0).getString("name")); + Assertions.assertEquals("admin@company.com", users.get(0).getString("email")); + Assertions.assertEquals("superuser", users.get(0).getList("roles", String.class).get(0)); + + Assertions.assertEquals("Backup", users.get(1).getString("name")); + Assertions.assertEquals("backup@company.com", users.get(1).getString("email")); + Assertions.assertEquals("readonly", users.get(1).getList("roles", String.class).get(0)); + } + + @Test + @DisplayName("GIVEN all Mongock changeUnits already executed " + + "AND skip import flag disabled (implicit) " + + "WHEN migrating to Flamingock Community " + + "THEN should import the entire history " + + "AND execute the pending flamingock changes") + void GIVEN_allMongockChangeUnitsAlreadyExecutedAndSkipImportFlagDisabledImplicit_WHEN_migratingToFlamingockCommunity_THEN_shouldImportEntireHistory() { + + // Setup Mongock entries + mongockTestHelper.setupBasicScenario(); + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + final String SKIP_IMPORT_VALUE = ""; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_SKIP_PROPERTY_KEY, SKIP_IMPORT_VALUE) // only allows empty / true / false + .build(); + + flamingock.run(); + + // Verify audit sequence: 11 total entries as shown in actual execution + // Legacy imports only show APPLIED (imported from Mongock), new changes show STARTED+APPLIED + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + APPLIED("system-change-00001_before"), + APPLIED("system-change-00001"), + APPLIED("mongock-change-1_before"), + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), + + // System stage - actual system importer change + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + // Application stage - new changes created by templates + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + + + // Validate actual change + List users = PublisherSync.collect(database.getCollection("users").find()); + + assertEquals(2, users.size()); + Assertions.assertEquals("Admin", users.get(0).getString("name")); + Assertions.assertEquals("admin@company.com", users.get(0).getString("email")); + Assertions.assertEquals("superuser", users.get(0).getList("roles", String.class).get(0)); + + Assertions.assertEquals("Backup", users.get(1).getString("name")); + Assertions.assertEquals("backup@company.com", users.get(1).getString("email")); + Assertions.assertEquals("readonly", users.get(1).getList("roles", String.class).get(0)); + } +} diff --git a/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactiveTest.java b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactiveTest.java index f3feaeaec..5823e656d 100644 --- a/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactiveTest.java +++ b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactiveTest.java @@ -38,13 +38,6 @@ import java.util.Date; import java.util.List; -/** - * Mirrors io.flamingock.importer.mongock.mongodb.MongoDBImporterTest's core assertions - * (mapping correctness + IGNORED-state filtering) for the reactive driver flavor. Full - * pipeline-level E2E proof (real upgrade via a booted JHipster reactive app with - * @MongockSupport wired to the reactive target system) lives in the JHipster fork's - * poc/gradle-sb4-reactive PoC. - */ @Testcontainers class MongockImporterMongoDBReactiveTest { @@ -99,16 +92,17 @@ void shouldMapExecutedEntry() { } @Test - @DisplayName("Should skip an IGNORED legacy entry without throwing") - void shouldSkipIgnoredEntry() { + @DisplayName("Should map an IGNORED legacy entry to null") + void shouldMapIgnoredEntryToNull() { seed(document("users-initialization", "EXECUTED", "EXECUTION", "pretend-mongock-run")); seed(document("ghost-extra", "IGNORED", "EXECUTION", null)); MongockImporterMongoDBReactive importer = new MongockImporterMongoDBReactive(database, LEGACY_COLLECTION); List history = importer.getAuditHistory(); - Assertions.assertEquals(1, history.size(), "IGNORED entry must be filtered out, not throw"); + Assertions.assertEquals(2, history.size()); Assertions.assertEquals("users-initialization", history.get(0).getChangeId()); + Assertions.assertNull(history.get(1)); } @Test diff --git a/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/changes/MongockChangeGroup.java b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/changes/MongockChangeGroup.java new file mode 100644 index 000000000..9c975a2af --- /dev/null +++ b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/changes/MongockChangeGroup.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.importer.mongock.mongodb.reactive.changes; + +import io.mongock.api.annotations.ChangeUnit; +import io.mongock.api.annotations.Execution; + +public class MongockChangeGroup { + + @ChangeUnit(id = "mongock-change-1", order = "1", author = "flamingock-team") + public static class MongockChange1 { + + @Execution + public void apply() { + System.out.println("Client Initializer"); + } + } + + @ChangeUnit(id = "mongock-change-2", order = "2", author = "flamingock-team") + public static class MongockChange2 { + + @Execution + public void apply() { + System.out.println("Client Initializer"); + } + } +} diff --git a/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/changes/_0003__CreateUsersCollectionChange.java b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/changes/_0003__CreateUsersCollectionChange.java new file mode 100644 index 000000000..eef77303c --- /dev/null +++ b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/changes/_0003__CreateUsersCollectionChange.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.importer.mongock.mongodb.reactive.changes; + +import com.mongodb.reactivestreams.client.MongoDatabase; +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.NonLockGuarded; +import io.flamingock.api.annotations.TargetSystem; +import io.flamingock.reactive.util.PublisherSync; + +@TargetSystem( id = "mongodb-target-system") +@Change(id = "create-users-collection-with-index", author = "bercianor", transactional = false) +public class _0003__CreateUsersCollectionChange { + + @Apply + public void apply(@NonLockGuarded MongoDatabase mongoDatabase) { + PublisherSync.complete(mongoDatabase.createCollection("users")); + } +} diff --git a/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/changes/_0004__SeedUsersChange.java b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/changes/_0004__SeedUsersChange.java new file mode 100644 index 000000000..51eb155f0 --- /dev/null +++ b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/changes/_0004__SeedUsersChange.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.importer.mongock.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.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.NonLockGuarded; +import io.flamingock.api.annotations.TargetSystem; +import io.flamingock.reactive.util.PublisherSync; +import org.bson.Document; + +@TargetSystem( id = "mongodb-target-system") +@Change(id = "seed-users", author = "bercianor") +public class _0004__SeedUsersChange { + + @Apply + public void apply(@NonLockGuarded MongoDatabase mongoDatabase, @NonLockGuarded ClientSession clientSession) { + MongoCollection collection = mongoDatabase.getCollection("users"); + PublisherSync.complete(collection.insertMany(clientSession, java.util.Arrays.asList( + new Document().append("name", "Admin").append("email", "admin@company.com").append("roles", java.util.Arrays.asList("superuser")), + new Document().append("name", "Backup").append("email", "backup@company.com").append("roles", java.util.Arrays.asList("readonly")) + ))); + } +} From b6b39f439204da031b97c80c87e1822d2c405455 Mon Sep 17 00:00:00 2001 From: davidfrigolet Date: Fri, 24 Jul 2026 15:47:18 +0100 Subject: [PATCH 3/3] fix(mongodb): mongodb reactive migration when ignored entries --- .../MongockImporterMongoDBReactive.java | 8 +++ .../MongoDBReactiveImporterE2ETest.java | 54 +++++++++++++++++++ .../MongockImporterMongoDBReactiveTest.java | 7 ++- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactive.java b/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactive.java index ed7a666d3..afcbfa25e 100644 --- a/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactive.java +++ b/legacy/mongock-importer-mongodb-reactive/src/main/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactive.java @@ -22,16 +22,21 @@ import io.flamingock.internal.common.core.audit.AuditHistoryReader; import io.flamingock.reactive.util.PublisherSync; import org.bson.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; public class MongockImporterMongoDBReactive implements AuditHistoryReader { + private static final Logger logger = LoggerFactory.getLogger("MongockImporter"); + private final MongoCollection sourceCollection; public MongockImporterMongoDBReactive(MongoDatabase mongoDatabase, String collectionName) { @@ -43,6 +48,7 @@ public List getAuditHistory() { return PublisherSync.collect(sourceCollection.find()) .stream() .map(MongockImporterMongoDBReactive::toAuditEntry) + .filter(Objects::nonNull) .collect(Collectors.toList()); } @@ -54,6 +60,8 @@ private static AuditEntry toAuditEntry(Document document) { .toLocalDateTime(); if (changeEntry.shouldBeIgnored()) { + logger.info("Skipping Mongock audit entry with changeId[{}]: state=IGNORED (Mongock never executed this change; nothing to import).", + changeEntry.getChangeId()); return null; } return new AuditEntry( diff --git a/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongoDBReactiveImporterE2ETest.java b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongoDBReactiveImporterE2ETest.java index a49a88763..f25c96e57 100644 --- a/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongoDBReactiveImporterE2ETest.java +++ b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongoDBReactiveImporterE2ETest.java @@ -24,6 +24,7 @@ import io.flamingock.api.annotations.Stage; import io.flamingock.common.test.mongock.MongockChangeEntry; import io.flamingock.common.test.mongock.MongockChangeState; +import io.flamingock.common.test.mongock.MongockChangeType; import io.flamingock.common.test.mongock.MongockTestHelper; import io.flamingock.core.kit.TestKit; import io.flamingock.core.kit.audit.AuditTestHelper; @@ -491,6 +492,59 @@ void GIVEN_unknownAuditEntriesAndRelaxedMode_WHEN_migratingToFlamingockCommunity ); } + @Test + @DisplayName("GIVEN Mongock audit history contains an IGNORED entry " + + "WHEN migrating to Flamingock Community " + + "THEN should skip the IGNORED entry without throwing " + + "AND import the rest of the history " + + "AND execute the pending flamingock changes") + void GIVEN_ignoredAuditEntry_WHEN_migratingToFlamingockCommunity_THEN_shouldSkipIgnoredAndImportRest() throws java.text.ParseException { + int written = mongockTestHelper.setupBasicScenario(); + mongockTestHelper.write(new MongockChangeEntry( + MongockTestHelper.DEFAULT_EXECUTION_ID, + "ghost-change", + "mongock", + MongockTestHelper.DEFAULT_DATE_FORMAT.parse("2025-06-19T05:43:57.190Z"), + MongockChangeState.IGNORED, + MongockChangeType.EXECUTION, + "io.example.ghost.GhostChangeUnit", + "apply", + null, + 0L, + MongockTestHelper.DEFAULT_HOSTNAME, + null, + false, + null + )); + assertEquals(5, written); + + MongoDBReactiveTargetSystem mongodbTargetSystem = new MongoDBReactiveTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .build(); + + flamingock.run(); + + // The IGNORED entry ("ghost-change") is skipped entirely — no audit row for it at all, + // not even a FAILED/IGNORED marker. Rest of the history imports and executes normally. + auditHelper.verifyAuditSequenceStrict( + APPLIED("system-change-00001_before"), + APPLIED("system-change-00001"), + APPLIED("mongock-change-1_before"), + APPLIED("mongock-change-1"), + APPLIED("mongock-change-2"), + + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + + STARTED("create-users-collection-with-index"), + APPLIED("create-users-collection-with-index"), + STARTED("seed-users"), + APPLIED("seed-users") + ); + } + @Test @DisplayName("GIVEN relaxed import flag with invalid value " + "WHEN migrating to Flamingock Community " + diff --git a/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactiveTest.java b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactiveTest.java index 5823e656d..b8eb09eb8 100644 --- a/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactiveTest.java +++ b/legacy/mongock-importer-mongodb-reactive/src/test/java/io/flamingock/importer/mongock/mongodb/reactive/MongockImporterMongoDBReactiveTest.java @@ -92,17 +92,16 @@ void shouldMapExecutedEntry() { } @Test - @DisplayName("Should map an IGNORED legacy entry to null") - void shouldMapIgnoredEntryToNull() { + @DisplayName("Should skip an IGNORED legacy entry without throwing") + void shouldSkipIgnoredEntry() { seed(document("users-initialization", "EXECUTED", "EXECUTION", "pretend-mongock-run")); seed(document("ghost-extra", "IGNORED", "EXECUTION", null)); MongockImporterMongoDBReactive importer = new MongockImporterMongoDBReactive(database, LEGACY_COLLECTION); List history = importer.getAuditHistory(); - Assertions.assertEquals(2, history.size()); + Assertions.assertEquals(1, history.size(), "IGNORED entry must be filtered out, not throw"); Assertions.assertEquals("users-initialization", history.get(0).getChangeId()); - Assertions.assertNull(history.get(1)); } @Test