diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/Constants.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/Constants.java index ffee9c37f..d3fa0730f 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/Constants.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/Constants.java @@ -28,6 +28,7 @@ public final class Constants { public static final String MONGOCK_IMPORT_SKIP_PROPERTY_KEY = "internal.mongock.import.skip"; public static final String MONGOCK_IMPORT_ORIGIN_PROPERTY_KEY = "internal.mongock.import.origin"; public static final String MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY = "internal.mongock.import.emptyOriginAllowed"; + public static final String MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_ENTRIES_PROPERTY_KEY = "internal.mongock.import.ignoreUnknownAuditEntries"; private Constants() {} diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/pipeline/PipelineHelper.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/pipeline/PipelineHelper.java index eaa43debf..d03a46d95 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/pipeline/PipelineHelper.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/pipeline/PipelineHelper.java @@ -16,32 +16,28 @@ package io.flamingock.internal.common.core.pipeline; import io.flamingock.internal.common.core.audit.AuditEntry; -import org.jetbrains.annotations.NotNull; + +import java.util.Optional; public class PipelineHelper { public static final String SYSTEM_STAGE_ID = "flamingock-system-stage"; public static final String LEGACY_STAGE_ID = "flamingock-legacy-stage"; - private static final String errorTemplate = "importing change with id[%s] from database. It must be imported to a flamingock stage"; - private final PipelineDescriptor pipelineDescriptor; public PipelineHelper(PipelineDescriptor pipelineDescriptor) { this.pipelineDescriptor = pipelineDescriptor; } - public String getStageId(AuditEntry auditEntryFromOrigin) { + public Optional findStageId(AuditEntry auditEntryFromOrigin) { if (Boolean.TRUE.equals(auditEntryFromOrigin.getSystemChange())) { - return LEGACY_STAGE_ID; - } else { - String changeIdInPipeline = getBaseChangeId(auditEntryFromOrigin); - return pipelineDescriptor.getStageByChange(changeIdInPipeline).orElseThrow(() -> generateChangeIdException(changeIdInPipeline)); + return Optional.of(LEGACY_STAGE_ID); } + String changeIdInPipeline = getBaseChangeId(auditEntryFromOrigin); + return pipelineDescriptor.getStageByChange(changeIdInPipeline); } - - public String getBaseChangeId(AuditEntry auditEntry) { String originalChangeId = auditEntry.getChangeId(); int index = originalChangeId.indexOf("_before"); @@ -51,9 +47,4 @@ public String getBaseChangeId(AuditEntry auditEntry) { public String getStorableChangeId(AuditEntry auditEntry) { return auditEntry.getChangeId(); } - - @NotNull - public IllegalArgumentException generateChangeIdException(String changeIdInPipeline) { - return new IllegalArgumentException(String.format(errorTemplate, changeIdInPipeline)); - } } diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/pipeline/PipelineHelperTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/pipeline/PipelineHelperTest.java new file mode 100644 index 000000000..2f1349cca --- /dev/null +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/pipeline/PipelineHelperTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.common.core.pipeline; + +import io.flamingock.api.RecoveryStrategy; +import io.flamingock.internal.common.core.audit.AuditEntry; +import io.flamingock.internal.common.core.change.ChangeDescriptor; +import io.flamingock.internal.common.core.context.ContextInjectable; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static io.flamingock.internal.common.core.pipeline.PipelineHelper.LEGACY_STAGE_ID; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PipelineHelperTest { + + private final PipelineHelper pipelineHelper = new PipelineHelper(new PipelineDescriptor() { + @Override + public Optional getLoadedChange(String changeId) { + return Optional.empty(); + } + + @Override + public Optional getStageByChange(String changeId) { + return "known-change".equals(changeId) ? Optional.of("user-stage") : Optional.empty(); + } + + @Override + public void contributeToContext(ContextInjectable contextInjectable) { + // no-op for unit test + } + }); + + @Test + void shouldReturnLegacyStageForSystemChange() { + Optional stageId = pipelineHelper.findStageId(buildAuditEntry("system-change-1", true)); + + assertEquals(Optional.of(LEGACY_STAGE_ID), stageId); + } + + @Test + void shouldReturnMatchingStageForKnownChange() { + Optional stageId = pipelineHelper.findStageId(buildAuditEntry("known-change", false)); + + assertEquals(Optional.of("user-stage"), stageId); + } + + @Test + void shouldReturnEmptyForUnknownChange() { + Optional stageId = pipelineHelper.findStageId(buildAuditEntry("unknown-change", false)); + + assertFalse(stageId.isPresent()); + } + + private static AuditEntry buildAuditEntry(String changeId, boolean systemChange) { + return new AuditEntry( + "exec-1", + null, + changeId, + "author", + LocalDateTime.now(), + AuditEntry.Status.APPLIED, + AuditEntry.ChangeType.MONGOCK_EXECUTION, + "io.example.Change", + "apply", + null, + 10L, + "host", + null, + systemChange, + null, + null, + null, + null, + RecoveryStrategy.MANUAL_INTERVENTION, + null + ); + } +} 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 7cf0dfa1b..a95ec1b2f 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 @@ -30,7 +30,6 @@ import io.flamingock.store.couchbase.CouchbaseAuditStore; import io.flamingock.internal.common.core.response.data.ErrorInfo; import io.flamingock.internal.common.couchbase.CouchbaseCollectionHelper; -import io.flamingock.internal.core.builder.FlamingockFactory; import io.flamingock.internal.core.builder.runner.Runner; import io.flamingock.internal.core.operation.StagedExecuteOperationException; import io.flamingock.internal.util.constants.CommunityPersistenceConstants; @@ -53,6 +52,7 @@ import static io.flamingock.core.kit.audit.AuditEntryExpectation.APPLIED; import static io.flamingock.core.kit.audit.AuditEntryExpectation.STARTED; 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_AUDIT_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 io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_CREATED_AT; @@ -320,6 +320,108 @@ void GIVEN_allMongockChangeUnitsAlreadyExecutedAndCustomOriginProvided_WHEN_migr } + @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() { + 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("foreign-change-1", createAuditObject("foreign-change-1", false, "io.example.foreign.ForeignChangeUnit", "apply")); + + CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); + + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .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() { + 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("foreign-change-1", createAuditObject("foreign-change-1", false, "io.example.foreign.ForeignChangeUnit", "apply")); + + CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); + + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .setProperty(MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_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() { + 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("foreign-change-1", createAuditObject("foreign-change-1", false, "io.example.foreign.ForeignChangeUnit", "apply")); + + CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); + + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .setProperty(MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_ENTRIES_PROPERTY_KEY, Boolean.TRUE.toString()) + .build(); + + flamingock.run(); + + auditHelper.verifyAuditSequenceStrict( + APPLIED("mongock-change-1"), + STARTED("migration-mongock-to-flamingock-community"), + APPLIED("migration-mongock-to-flamingock-community"), + STARTED("mongock-change-2"), + APPLIED("mongock-change-2"), + STARTED("flamingock-change"), + APPLIED("flamingock-change") + ); + } + + @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() { + Collection originCollection = cluster.bucket(MONGOCK_BUCKET_NAME).scope(MONGOCK_SCOPE_NAME).collection(MONGOCK_COLLECTION_NAME); + originCollection.upsert("foreign-change-1", createAuditObject("foreign-change-1", false, "io.example.foreign.ForeignChangeUnit", "apply")); + + final String flagValue = "invalid_value"; + + CouchbaseTargetSystem targetSystem = new CouchbaseTargetSystem("couchbase-target-system", cluster, FLAMINGOCK_BUCKET_NAME); + + Runner flamingock = testKit.createBuilder() + .setAuditStore(auditStore) + .addTargetSystem(targetSystem) + .setProperty(MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_ENTRIES_PROPERTY_KEY, flagValue) + .build(); + + StagedExecuteOperationException ex = assertThrows(StagedExecuteOperationException.class, flamingock::run); + assertEquals("Invalid value for " + MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_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" + @@ -481,6 +583,10 @@ private List getAuditLog() { } private static JsonObject createAuditObject(String value) { + return createAuditObject(value, true, "io.flamingock.changelog.Class1", "method1"); + } + + private static JsonObject createAuditObject(String value, boolean systemChange, String changeLogClass, String changeSetMethod) { JsonObject doc = JsonObject.create() .put("executionId", "exec-1") .put("changeId", value) @@ -488,13 +594,13 @@ private static JsonObject createAuditObject(String value) { .put("timestamp", Instant.now().toEpochMilli()) .put("state", "EXECUTED") .put("type", "EXECUTION") - .put("changeLogClass", "io.flamingock.changelog.Class1") - .put("changeSetMethod", "method1") + .put("changeLogClass", changeLogClass) + .put("changeSetMethod", changeSetMethod) .putNull("metadata") .put("executionMillis", 123L) .put("executionHostName", "host1") .putNull("errorTrace") - .put("systemChange", true) + .put("systemChange", systemChange) .put("_doctype", "mongockChangeEntry"); return doc; } 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 9cd64eeba..56b478ad8 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 @@ -49,6 +49,7 @@ import static io.flamingock.core.kit.audit.AuditEntryExpectation.STARTED; 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_AUDIT_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; @@ -350,6 +351,97 @@ void GIVEN_allMongockChangeUnitsAlreadyExecutedAndCustomOriginProvided_WHEN_migr assertEquals(KeyType.HASH, tableDescription.table().keySchema().get(0).keyType()); } + @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(); + + DynamoDBTargetSystem dynamodbTargetSystem = new DynamoDBTargetSystem("dynamodb-target-system", client); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(dynamodbTargetSystem) + .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(); + + DynamoDBTargetSystem dynamodbTargetSystem = new DynamoDBTargetSystem("dynamodb-target-system", client); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(dynamodbTargetSystem) + .setProperty(MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_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(); + + DynamoDBTargetSystem dynamodbTargetSystem = new DynamoDBTargetSystem("dynamodb-target-system", client); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(dynamodbTargetSystem) + .setProperty(MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_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-table"), + APPLIED("create-users-table") + ); + } + + @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(); + + DynamoDBTargetSystem dynamodbTargetSystem = new DynamoDBTargetSystem("dynamodb-target-system", client); + + final String flagValue = "invalid_value"; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(dynamodbTargetSystem) + .setProperty(MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_ENTRIES_PROPERTY_KEY, flagValue) + .build(); + + StagedExecuteOperationException ex = assertThrows(StagedExecuteOperationException.class, flamingock::run); + assertEquals("Invalid value for " + MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_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" + 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 58f56447a..32e9eafc6 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 @@ -49,6 +49,7 @@ import static io.flamingock.core.kit.audit.AuditEntryExpectation.STARTED; 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_AUDIT_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; @@ -372,6 +373,99 @@ void GIVEN_allMongockChangeUnitsAlreadyExecutedAndCustomOriginProvidedByLiteralV Assertions.assertEquals("readonly", users.get(1).getList("roles", String.class).get(0)); } + @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(); + + MongoDBSyncTargetSystem mongodbTargetSystem = new MongoDBSyncTargetSystem("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(); + + MongoDBSyncTargetSystem mongodbTargetSystem = new MongoDBSyncTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_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(); + + MongoDBSyncTargetSystem mongodbTargetSystem = new MongoDBSyncTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_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(); + + MongoDBSyncTargetSystem mongodbTargetSystem = new MongoDBSyncTargetSystem("mongodb-target-system", mongoClient, DATABASE_NAME); + + final String flagValue = "invalid_value"; + + Runner flamingock = testKit.createBuilder() + .addTargetSystem(mongodbTargetSystem) + .setProperty(MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_ENTRIES_PROPERTY_KEY, flagValue) + .build(); + + StagedExecuteOperationException ex = assertThrows(StagedExecuteOperationException.class, flamingock::run); + assertEquals("Invalid value for " + MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_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" + diff --git a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/MongockImportChange.java b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/MongockImportChange.java index d5c5e4892..8d5ac0c1d 100644 --- a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/MongockImportChange.java +++ b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/MongockImportChange.java @@ -31,9 +31,11 @@ import javax.inject.Named; import java.util.List; +import java.util.Optional; import static io.flamingock.internal.common.core.audit.AuditReaderType.MONGOCK; 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_AUDIT_ENTRIES_PROPERTY_KEY; import static io.flamingock.internal.common.core.metadata.Constants.MONGOCK_IMPORT_SKIP_PROPERTY_KEY; /** @@ -49,7 +51,8 @@ public void importHistory(@Named("change.targetSystem.id") String targetSystemId @NonLockGuarded AuditWriter auditWriter, @NonLockGuarded PipelineDescriptor pipelineDescriptor, @Nullable @Named(MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY) String emptyOriginAllowed, - @Nullable @Named(MONGOCK_IMPORT_SKIP_PROPERTY_KEY) String skipImport) { + @Nullable @Named(MONGOCK_IMPORT_SKIP_PROPERTY_KEY) String skipImport, + @Nullable @Named(MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_ENTRIES_PROPERTY_KEY) String ignoreUnknownAuditEntries) { if (resolveSkipImport(skipImport)) { logger.info("Mongock audit log import skipped (skipImport=true). No audit entries will be migrated."); return; @@ -58,12 +61,26 @@ public void importHistory(@Named("change.targetSystem.id") String targetSystemId AuditHistoryReader legacyHistoryReader = getAuditHistoryReader(targetSystemId, targetSystemManager); PipelineHelper pipelineHelper = new PipelineHelper(pipelineDescriptor); List legacyHistory = legacyHistoryReader.getAuditHistory(); + boolean ignoreUnknownEntries = resolveIgnoreUnknownAuditEntries(ignoreUnknownAuditEntries); validate(legacyHistory, targetSystemId, emptyOriginAllowed); legacyHistory.forEach(auditEntryFromOrigin -> { + Optional stageId = pipelineHelper.findStageId(auditEntryFromOrigin); + if (!stageId.isPresent()) { + if (ignoreUnknownEntries) { + logger.warn("Ignored audit entry with changeId[{}] while importing audit history: no matching change was found in the current Flamingock pipeline. changeLogClass[{}], changeSetMethod[{}]", + auditEntryFromOrigin.getChangeId(), + auditEntryFromOrigin.getClassName(), + auditEntryFromOrigin.getMethodName()); + return; + } + throw new FlamingockException(String.format( + "Error importing audit entry with changeId[%s]: no matching change was found in the current Flamingock pipeline.", + pipelineHelper.getBaseChangeId(auditEntryFromOrigin))); + } //This is the changeId present in the pipeline. If it's a system change or '..._before' won't appear AuditEntry auditEntryWithStageId = auditEntryFromOrigin.copyWithNewIdAndStageId( pipelineHelper.getStorableChangeId(auditEntryFromOrigin), - pipelineHelper.getStageId(auditEntryFromOrigin)); + stageId.get()); auditWriter.writeEntry(auditEntryWithStageId); }); } @@ -98,24 +115,25 @@ private void validate(List legacyHistory, String targetSystemId, Str } private boolean resolveEmptyOriginAllowed(String raw) { - if (raw == null || raw.trim().isEmpty()) { - return false; // default behaviour - } - String v = raw.trim(); - if ("true".equalsIgnoreCase(v)) return true; - if ("false".equalsIgnoreCase(v)) return false; - throw new FlamingockException("Invalid value for " + MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY + ": " + raw - + " (expected \"true\" or \"false\" or empty)"); + return resolveBooleanPropertyValue(raw, MONGOCK_IMPORT_EMPTY_ORIGIN_ALLOWED_PROPERTY_KEY); } private boolean resolveSkipImport(String raw) { + return resolveBooleanPropertyValue(raw, MONGOCK_IMPORT_SKIP_PROPERTY_KEY); + } + + private boolean resolveIgnoreUnknownAuditEntries(String raw) { + return resolveBooleanPropertyValue(raw, MONGOCK_IMPORT_IGNORE_UNKNOWN_AUDIT_ENTRIES_PROPERTY_KEY); + } + + private boolean resolveBooleanPropertyValue(String raw, String propertyName) { if (raw == null || raw.trim().isEmpty()) { return false; // default behaviour } String v = raw.trim(); if ("true".equalsIgnoreCase(v)) return true; if ("false".equalsIgnoreCase(v)) return false; - throw new FlamingockException("Invalid value for " + MONGOCK_IMPORT_SKIP_PROPERTY_KEY + ": " + raw + throw new FlamingockException("Invalid value for " + propertyName + ": " + raw + " (expected \"true\" or \"false\" or empty)"); } } diff --git a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/annotations/MongockSupport.java b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/annotations/MongockSupport.java index 22e12d71b..8f2a3e6a4 100644 --- a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/annotations/MongockSupport.java +++ b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/annotations/MongockSupport.java @@ -127,4 +127,20 @@ * @return {@code "true"} to allow empty origin, {@code "false"} to fail; empty treated as {@code "false"} */ String emptyOriginAllowed() default ""; + + /** + * Determines whether Flamingock should skip imported Mongock audit entries that do not + * match any change in the current Flamingock pipeline. + *

+ * Expected literal values are {@code "true"} or {@code "false"}. + *

+ * + *

+ * If empty (default), it will be treated as {@code "false"} and Flamingock will preserve + * the current strict behaviour. + *

+ * + * @return {@code "true"} to skip unknown imported entries, {@code "false"} to fail; empty treated as {@code "false"} + */ + String ignoreUnknownAuditEntries() default ""; } diff --git a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/processor/MongockAnnotationProcessorPlugin.java b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/processor/MongockAnnotationProcessorPlugin.java index e2ac55153..a7ade7934 100644 --- a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/processor/MongockAnnotationProcessorPlugin.java +++ b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/processor/MongockAnnotationProcessorPlugin.java @@ -52,6 +52,7 @@ 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_SKIP_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_IGNORE_UNKNOWN_AUDIT_ENTRIES_PROPERTY_KEY; @SuppressWarnings("deprecation") public class MongockAnnotationProcessorPlugin implements AnnotationProcessorPlugin, ChangeDiscoverer, ConfigurationPropertiesProvider { @@ -164,5 +165,12 @@ private void processConfigurationProperties(MongockSupport mongockSupport, Map