diff --git a/legacy/mongock-importer-couchbase/src/main/java/io/flamingock/importer/mongock/couchbase/CouchbaseChangeEntry.java b/legacy/mongock-importer-couchbase/src/main/java/io/flamingock/importer/mongock/couchbase/CouchbaseChangeEntry.java index 4448ed862..bf93ac589 100644 --- a/legacy/mongock-importer-couchbase/src/main/java/io/flamingock/importer/mongock/couchbase/CouchbaseChangeEntry.java +++ b/legacy/mongock-importer-couchbase/src/main/java/io/flamingock/importer/mongock/couchbase/CouchbaseChangeEntry.java @@ -46,6 +46,10 @@ public void setExecutionId(String executionId) { this.executionId = executionId; } + public String getChangeId() { + return changeId; + } + public static CouchbaseChangeEntry fromJson(JsonObject doc) { CouchbaseChangeEntry entry = new CouchbaseChangeEntry(); entry.executionId = doc.getString("executionId"); @@ -71,7 +75,15 @@ private static Long parseLong(Object value) { throw new IllegalArgumentException("Cannot convert value to Long: " + value); } + public boolean shouldBeIgnored() { + return MongockChangeState.valueOf(state) == MongockChangeState.IGNORED; + } + public AuditEntry toAuditEntry() { + if (shouldBeIgnored()) { + return null; + } + LocalDateTime ts = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()); MongockChangeState stateEnum = MongockChangeState.valueOf(state); diff --git a/legacy/mongock-importer-couchbase/src/main/java/io/flamingock/importer/mongock/couchbase/MongockImporterCouchbase.java b/legacy/mongock-importer-couchbase/src/main/java/io/flamingock/importer/mongock/couchbase/MongockImporterCouchbase.java index 9780fa9aa..e1f19520a 100644 --- a/legacy/mongock-importer-couchbase/src/main/java/io/flamingock/importer/mongock/couchbase/MongockImporterCouchbase.java +++ b/legacy/mongock-importer-couchbase/src/main/java/io/flamingock/importer/mongock/couchbase/MongockImporterCouchbase.java @@ -24,6 +24,7 @@ import io.flamingock.internal.common.core.audit.AuditHistoryReader; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; public class MongockImporterCouchbase implements AuditHistoryReader { @@ -56,7 +57,15 @@ public List getAuditHistory() { return result.rowsAsObject().stream() .map(CouchbaseChangeEntry::fromJson) - .map(CouchbaseChangeEntry::toAuditEntry) + .map(MongockImporterCouchbase::toAuditEntry) + .filter(Objects::nonNull) .collect(Collectors.toList()); } + + private static AuditEntry toAuditEntry(CouchbaseChangeEntry entry) { + if (entry.shouldBeIgnored()) { + return null; + } + return entry.toAuditEntry(); + } } \ No newline at end of file diff --git a/legacy/mongock-importer-couchbase/src/test/java/io/flamingock/importer/mongock/couchbase/CouchbaseImporterTest.java b/legacy/mongock-importer-couchbase/src/test/java/io/flamingock/importer/mongock/couchbase/CouchbaseImporterTest.java index 38d721510..c44f2990d 100644 --- a/legacy/mongock-importer-couchbase/src/test/java/io/flamingock/importer/mongock/couchbase/CouchbaseImporterTest.java +++ b/legacy/mongock-importer-couchbase/src/test/java/io/flamingock/importer/mongock/couchbase/CouchbaseImporterTest.java @@ -204,6 +204,40 @@ void GIVEN_someChangeUnitsAlreadyExecuted_WHEN_migratingToFlamingockCommunity_TH } + @Test + @DisplayName("GIVEN Mongock audit history contains an IGNORED entry " + + "WHEN migrating to Flamingock Community " + + "THEN should skip the IGNORED entry without crashing " + + "AND import the rest of the history normally") + void GIVEN_ignoredAuditEntry_WHEN_migratingToFlamingockCommunity_THEN_shouldSkipIgnoredAndImportRest() { + Collection originCollection = cluster.bucket(MONGOCK_BUCKET_NAME).scope(MONGOCK_SCOPE_NAME).collection(MONGOCK_COLLECTION_NAME); + + originCollection.upsert("mongock-change-1", createAuditObject("mongock-change-1")); + originCollection.upsert("mongock-change-2", createAuditObject("mongock-change-2")); + originCollection.upsert("ignored-change", createAuditObject("ignored-change", true, "io.example.IgnoredChangeUnit", "apply", "IGNORED")); + + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .build(); + + flamingock.run(); + + auditHelper.verifyAuditSequenceStrict( + // Legacy imports from Mongock (APPLIED only - no STARTED for imported changes) + 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 + STARTED("flamingock-change"), + APPLIED("flamingock-change") + ); + } + @Test @DisplayName("GIVEN mongock audit history empty " + "AND no empty origen allowed value provided " + @@ -587,12 +621,16 @@ private static JsonObject createAuditObject(String value) { } private static JsonObject createAuditObject(String value, boolean systemChange, String changeLogClass, String changeSetMethod) { + return createAuditObject(value, systemChange, changeLogClass, changeSetMethod, "EXECUTED"); + } + + private static JsonObject createAuditObject(String value, boolean systemChange, String changeLogClass, String changeSetMethod, String state) { JsonObject doc = JsonObject.create() .put("executionId", "exec-1") .put("changeId", value) .put("author", "author1") .put("timestamp", Instant.now().toEpochMilli()) - .put("state", "EXECUTED") + .put("state", state) .put("type", "EXECUTION") .put("changeLogClass", changeLogClass) .put("changeSetMethod", changeSetMethod) diff --git a/legacy/mongock-importer-dynamodb/src/main/java/io/flamingock/importer/mongock/dynamodb/MongockAuditEntry.java b/legacy/mongock-importer-dynamodb/src/main/java/io/flamingock/importer/mongock/dynamodb/MongockAuditEntry.java index d1e05b55e..6c56abbea 100644 --- a/legacy/mongock-importer-dynamodb/src/main/java/io/flamingock/importer/mongock/dynamodb/MongockAuditEntry.java +++ b/legacy/mongock-importer-dynamodb/src/main/java/io/flamingock/importer/mongock/dynamodb/MongockAuditEntry.java @@ -180,7 +180,15 @@ public void setSystemChange(Boolean systemChange) { this.systemChange = systemChange; } + public boolean shouldBeIgnored() { + return MongockChangeState.valueOf(state) == MongockChangeState.IGNORED; + } + public AuditEntry toAuditEntry() { + if (shouldBeIgnored()) { + return null; + } + long epochMillis; try { epochMillis = Long.parseLong(timestamp); diff --git a/legacy/mongock-importer-dynamodb/src/main/java/io/flamingock/importer/mongock/dynamodb/MongockImporterDynamoDB.java b/legacy/mongock-importer-dynamodb/src/main/java/io/flamingock/importer/mongock/dynamodb/MongockImporterDynamoDB.java index 603ec3276..eeb0630b0 100644 --- a/legacy/mongock-importer-dynamodb/src/main/java/io/flamingock/importer/mongock/dynamodb/MongockImporterDynamoDB.java +++ b/legacy/mongock-importer-dynamodb/src/main/java/io/flamingock/importer/mongock/dynamodb/MongockImporterDynamoDB.java @@ -23,6 +23,7 @@ import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -44,7 +45,15 @@ public List getAuditHistory() { .collect(Collectors.toList()); return entries.stream() - .map(MongockAuditEntry::toAuditEntry) + .map(MongockImporterDynamoDB::toAuditEntry) + .filter(Objects::nonNull) .collect(Collectors.toList()); } + + private static AuditEntry toAuditEntry(MongockAuditEntry entry) { + if (entry.shouldBeIgnored()) { + return null; + } + return entry.toAuditEntry(); + } } diff --git a/legacy/mongock-importer-dynamodb/src/test/java/io/flamingock/importer/mongock/dynamodb/DynamoDBImporterTest.java b/legacy/mongock-importer-dynamodb/src/test/java/io/flamingock/importer/mongock/dynamodb/DynamoDBImporterTest.java index d4818fbcf..4032417bf 100644 --- a/legacy/mongock-importer-dynamodb/src/test/java/io/flamingock/importer/mongock/dynamodb/DynamoDBImporterTest.java +++ b/legacy/mongock-importer-dynamodb/src/test/java/io/flamingock/importer/mongock/dynamodb/DynamoDBImporterTest.java @@ -17,6 +17,8 @@ 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.store.dynamodb.DynamoDBAuditStore; import io.flamingock.core.kit.TestKit; import io.flamingock.core.kit.audit.AuditTestHelper; @@ -420,6 +422,51 @@ 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 crashing " + + "AND import the rest of the history normally") + void GIVEN_ignoredAuditEntry_WHEN_migratingToFlamingockCommunity_THEN_shouldSkipIgnoredAndImportRest() throws java.text.ParseException { + mongockTestHelper.setupBasicScenario(); + mongockTestHelper.write(new MongockChangeEntry( + "ignored-execution-1", + "ignored-change", + "mongock", + io.flamingock.common.test.mongock.MongockTestHelper.DEFAULT_DATE_FORMAT.parse("2025-06-19T05:43:57.200Z"), + MongockChangeState.IGNORED, + io.flamingock.common.test.mongock.MongockChangeType.EXECUTION, + "io.example.IgnoredChangeUnit", + "apply", + null, + 0L, + io.flamingock.common.test.mongock.MongockTestHelper.DEFAULT_HOSTNAME, + null, + false, + null + )); + + DynamoDBTargetSystem dynamodbTargetSystem = new DynamoDBTargetSystem("dynamodb-target-system", client); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(dynamodbTargetSystem) + .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-table"), + APPLIED("create-users-table") + ); + } + @Test @DisplayName("GIVEN relaxed import flag with invalid value " + "WHEN migrating to Flamingock Community " + 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..787a07744 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 @@ -28,6 +28,7 @@ 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 { @@ -43,19 +44,21 @@ 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()) { return null; } + + LocalDateTime timestamp = Instant.ofEpochMilli(changeEntry.getTimestamp().getTime()) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); return new AuditEntry( changeEntry.getExecutionId(), null, 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..e2ad7cf16 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") + 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()); Assertions.assertEquals("users-initialization", history.get(0).getChangeId()); - Assertions.assertNull(history.get(1)); } @Test diff --git a/legacy/mongock-importer-mongodb/src/main/java/io/flamingock/importer/mongock/mongodb/MongockImporterMongoDB.java b/legacy/mongock-importer-mongodb/src/main/java/io/flamingock/importer/mongock/mongodb/MongockImporterMongoDB.java index 0ba220ebe..36c3e3e8c 100644 --- a/legacy/mongock-importer-mongodb/src/main/java/io/flamingock/importer/mongock/mongodb/MongockImporterMongoDB.java +++ b/legacy/mongock-importer-mongodb/src/main/java/io/flamingock/importer/mongock/mongodb/MongockImporterMongoDB.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; public class MongockImporterMongoDB implements AuditHistoryReader { @@ -44,6 +45,7 @@ public List getAuditHistory() { .into(new ArrayList<>()) .stream() .map(MongockImporterMongoDB::toAuditEntry) + .filter(Objects::nonNull) .collect(Collectors.toList()); } diff --git a/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/MongoDBImporterTest.java b/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/MongoDBImporterTest.java index fa4fad124..8ce564540 100644 --- a/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/MongoDBImporterTest.java +++ b/legacy/mongock-importer-mongodb/src/test/java/io/flamingock/importer/mongock/mongodb/MongoDBImporterTest.java @@ -498,6 +498,65 @@ 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 crashing " + + "AND import the rest of the history normally") + void GIVEN_ignoredAuditEntry_WHEN_migratingToFlamingockCommunity_THEN_shouldSkipIgnoredAndImportRest() throws java.text.ParseException { + // Regression test for IGNORED-state null leak in MongockImporterMongoDB.toAuditEntry(). + // Before the fix, the importer returned null for IGNORED entries and the caller + // (MongockImportChange.importHistory) crashed with: + // NullPointerException: Cannot invoke "AuditEntry.getSystemChange()" + // because "auditEntryFromOrigin" is null + // The fix filters nulls at both layers and logs the skipped entry. + + mongockTestHelper.setupBasicScenario(); + mongockTestHelper.write(new MongockChangeEntry( + "ignored-execution-1", + "ignored-change", + "mongock", + MongockTestHelper.DEFAULT_DATE_FORMAT.parse("2025-06-19T05:43:57.200Z"), + MongockChangeState.IGNORED, + io.flamingock.common.test.mongock.MongockChangeType.EXECUTION, + "io.example.IgnoredChangeUnit", + "apply", + null, + 0L, + MongockTestHelper.DEFAULT_HOSTNAME, + null, + false, + null + )); + + MongoDBSyncTargetSystem mongodbTargetSystem = new MongoDBSyncTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .build(); + + flamingock.run(); + + // IGNORED entry must NOT appear in the Flamingock audit store. + assertNull(getAuditEntryByChangeId("ignored-change"), + "IGNORED Mongock entry must not be imported into the Flamingock audit store"); + + // Remaining basic-scenario entries imported as normal, plus native changes executed. + 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 " +