diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java index 9890ec4ae..de8bdcaf9 100644 --- a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java @@ -21,9 +21,72 @@ import io.flamingock.api.annotations.Nullable; import io.flamingock.api.annotations.Rollback; import io.flamingock.api.template.AbstractChangeTemplate; +import io.flamingock.internal.util.log.FlamingockLoggerFactory; +import io.flamingock.template.mongodb.model.MongoApplyPayload; import io.flamingock.template.mongodb.model.MongoOperation; +import org.slf4j.Logger; -public class MongoChangeTemplate extends AbstractChangeTemplate { +import java.util.ArrayList; +import java.util.List; + +/** + * MongoDB Change Template for executing declarative MongoDB operations defined in YAML. + * + *

Apply Behavior

+ *

The {@link #apply} method executes all operations defined in the payload sequentially. + * The behavior differs based on the transactional mode:

+ * + *

Transactional Mode ({@code transactional: true})

+ * + * + *

Non-Transactional Mode ({@code transactional: false})

+ * + * + *

YAML Example

+ *
{@code
+ * id: create-orders-collection
+ * transactional: false
+ * template: MongoChangeTemplate
+ * targetSystem:
+ *   id: "mongodb"
+ * apply:
+ *   operations:
+ *     - type: createCollection
+ *       collection: orders
+ *       rollback:
+ *         type: dropCollection
+ *         collection: orders
+ *
+ *     - type: insert
+ *       collection: orders
+ *       parameters:
+ *         documents:
+ *           - orderId: "ORD-001"
+ *             customer: "John Doe"
+ *       rollback:
+ *         type: delete
+ *         collection: orders
+ *         parameters:
+ *           filter: {}
+ * }
+ * + * @see MongoOperation + * @see MongoApplyPayload + */ +public class MongoChangeTemplate extends AbstractChangeTemplate { + + private static final Logger logger = FlamingockLoggerFactory.getLogger(MongoChangeTemplate.class); public MongoChangeTemplate() { super(MongoOperation.class); @@ -34,7 +97,7 @@ public void apply(MongoDatabase db, @Nullable ClientSession clientSession) { if (this.isTransactional && clientSession == null) { throw new IllegalArgumentException(String.format("Transactional change[%s] requires transactional ecosystem with ClientSession", changeId)); } - executeOp(db, applyPayload, clientSession); + executeOperationsWithAutoRollback(db, applyPayload, clientSession); } @Rollback @@ -42,11 +105,62 @@ public void rollback(MongoDatabase db, @Nullable ClientSession clientSession) { if (this.isTransactional && clientSession == null) { throw new IllegalArgumentException(String.format("Transactional change[%s] requires transactional ecosystem with ClientSession", changeId)); } - executeOp(db, rollbackPayload, clientSession); + executeRollbackOperations(db, applyPayload, clientSession); } - private void executeOp(MongoDatabase db, MongoOperation op, ClientSession clientSession) { - op.getOperator(db).apply(clientSession); + private void executeOperationsWithAutoRollback(MongoDatabase db, MongoApplyPayload payload, ClientSession clientSession) { + if (payload == null) { + return; + } + + List operations = payload.getOperations(); + + // Transactional, MongoDB handles rollback + if (this.isTransactional && clientSession != null) { + for (MongoOperation op : operations) { + op.getOperator(db).apply(clientSession); + } + return; + } + + // Non-transactional: auto-rollback on failure + List successfulOps = new ArrayList<>(); + for (MongoOperation op : operations) { + try { + op.getOperator(db).apply(clientSession); + successfulOps.add(op); + } catch (Exception e) { + rollbackSuccessfulOperations(db, successfulOps, clientSession); + throw e; + } + } + } + + private void rollbackSuccessfulOperations(MongoDatabase db, List successfulOps, ClientSession clientSession) { + for (int i = successfulOps.size() - 1; i >= 0; i--) { + MongoOperation op = successfulOps.get(i); + if (op.getRollback() != null) { + try { + op.getRollback().getOperator(db).apply(clientSession); + } catch (Exception rollbackEx) { + logger.warn("Rollback failed for operation: {}", op, rollbackEx); + } + } + } + } + + private void executeRollbackOperations(MongoDatabase db, MongoApplyPayload payload, ClientSession clientSession) { + if (payload == null) { + return; + } + + List operations = payload.getOperations(); + for (int i = operations.size() - 1; i >= 0; i--) { + MongoOperation op = operations.get(i); + if (op.getRollback() != null) { + op.getRollback().getOperator(db).apply(clientSession); + } + } } } \ No newline at end of file diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/mapper/CreateViewOptionsMapper.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/mapper/CreateViewOptionsMapper.java new file mode 100644 index 000000000..a528bbe3b --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/mapper/CreateViewOptionsMapper.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 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.template.mongodb.mapper; + +import com.mongodb.client.model.CreateViewOptions; + +import java.util.Map; + +import static io.flamingock.template.mongodb.mapper.MapperUtil.getCollation; + +public final class CreateViewOptionsMapper { + + private CreateViewOptionsMapper() {} + + public static CreateViewOptions map(Map options) { + CreateViewOptions result = new CreateViewOptions(); + + if (options.containsKey("collation")) { + result.collation(getCollation(options, "collation")); + } + + return result; + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/mapper/RenameCollectionOptionsMapper.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/mapper/RenameCollectionOptionsMapper.java new file mode 100644 index 000000000..ade56d87b --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/mapper/RenameCollectionOptionsMapper.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 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.template.mongodb.mapper; + +import com.mongodb.client.model.RenameCollectionOptions; + +import java.util.Map; + +import static io.flamingock.template.mongodb.mapper.MapperUtil.getBoolean; + +public final class RenameCollectionOptionsMapper { + + private RenameCollectionOptionsMapper() {} + + public static RenameCollectionOptions map(Map options) { + RenameCollectionOptions result = new RenameCollectionOptions(); + + if (options.containsKey("dropTarget")) { + result.dropTarget(getBoolean(options, "dropTarget")); + } + + return result; + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/MongoApplyPayload.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/MongoApplyPayload.java new file mode 100644 index 000000000..2ce371274 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/MongoApplyPayload.java @@ -0,0 +1,115 @@ +/* + * Copyright 2025 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.template.mongodb.model; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Container class for MongoDB apply/rollback payload that supports both: + *
    + *
  • Single operation format (backward compatible): + *
    + *     apply:
    + *       type: createCollection
    + *       collection: users
    + *     
    + *
  • + *
  • Multiple operations format: + *
    + *     apply:
    + *       operations:
    + *         - type: createCollection
    + *           collection: users
    + *         - type: createIndex
    + *           collection: users
    + *           parameters:
    + *             keys: { name: 1 }
    + *     
    + *
  • + *
+ */ +public class MongoApplyPayload { + + private List operations; + + // For backward compatibility + private String type; + private String collection; + private Map parameters; + + /** + * Returns the list of operations to execute. + * Handles both formats: + *
    + *
  • Multiple: returns the operations list directly
  • + *
  • Single: wraps the single operation in a list
  • + *
+ * + * @return list of operations to execute, never null + */ + public List getOperations() { + if (operations != null && !operations.isEmpty()) { + return operations; + } + if (type != null) { + MongoOperation singleOp = new MongoOperation(); + singleOp.setType(type); + singleOp.setCollection(collection); + singleOp.setParameters(parameters != null ? parameters : new HashMap<>()); + return Collections.singletonList(singleOp); + } + return Collections.emptyList(); + } + + public void setOperations(List operations) { + this.operations = operations; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getCollection() { + return collection; + } + + public void setCollection(String collection) { + this.collection = collection; + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + @Override + public String toString() { + if (operations != null && !operations.isEmpty()) { + return "MongoApplyPayload{operations=" + operations + "}"; + } + return "MongoApplyPayload{type='" + type + "', collection='" + collection + "', parameters=" + parameters + "}"; + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java index 4d931e157..80460a487 100644 --- a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java @@ -30,6 +30,7 @@ public class MongoOperation { private String type; private String collection; private Map parameters; + private MongoOperation rollback; public String getType() { return type; } @@ -43,6 +44,10 @@ public class MongoOperation { public void setParameters(Map parameters) { this.parameters = parameters; } + public MongoOperation getRollback() { return rollback; } + + public void setRollback(MongoOperation rollback) { this.rollback = rollback; } + @SuppressWarnings("unchecked") public List getDocuments() { return ((List>) parameters.get("documents")) @@ -69,6 +74,40 @@ public Document getFilter() { return new Document((Map) parameters.get("filter")); } + public String getIndexName() { + Object value = parameters.get("indexName"); + return value != null ? (String) value : null; + } + + public String getTarget() { + return (String) parameters.get("target"); + } + + @SuppressWarnings("unchecked") + public Document getValidator() { + Object value = parameters.get("validator"); + return value != null ? new Document((Map) value) : null; + } + + public String getValidationLevel() { + return (String) parameters.get("validationLevel"); + } + + public String getValidationAction() { + return (String) parameters.get("validationAction"); + } + + public String getViewOn() { + return (String) parameters.get("viewOn"); + } + + @SuppressWarnings("unchecked") + public List getPipeline() { + List> rawPipeline = (List>) parameters.get("pipeline"); + return rawPipeline != null + ? rawPipeline.stream().map(Document::new).collect(Collectors.toList()) + : null; + } public MongoOperator getOperator(MongoDatabase db) { return MongoOperationType.getFromValue(getType()).getOperator(db, this); @@ -76,10 +115,13 @@ public MongoOperator getOperator(MongoDatabase db) { @Override public String toString() { - final StringBuffer sb = new StringBuffer("MongoOperation{"); + final StringBuilder sb = new StringBuilder("MongoOperation{"); sb.append("type='").append(type).append('\''); sb.append(", collection='").append(collection).append('\''); sb.append(", parameters=").append(parameters); + if (rollback != null) { + sb.append(", rollback=").append(rollback); + } sb.append('}'); return sb.toString(); } diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java index f54c893e2..2180591e0 100644 --- a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java @@ -18,8 +18,15 @@ import com.mongodb.client.MongoDatabase; import io.flamingock.template.mongodb.model.operator.CreateCollectionOperator; import io.flamingock.template.mongodb.model.operator.CreateIndexOperator; +import io.flamingock.template.mongodb.model.operator.CreateViewOperator; +import io.flamingock.template.mongodb.model.operator.DeleteOperator; +import io.flamingock.template.mongodb.model.operator.DropCollectionOperator; +import io.flamingock.template.mongodb.model.operator.DropIndexOperator; +import io.flamingock.template.mongodb.model.operator.DropViewOperator; import io.flamingock.template.mongodb.model.operator.InsertOperator; +import io.flamingock.template.mongodb.model.operator.ModifyCollectionOperator; import io.flamingock.template.mongodb.model.operator.MongoOperator; +import io.flamingock.template.mongodb.model.operator.RenameCollectionOperator; import java.util.Arrays; import java.util.function.BiFunction; @@ -28,7 +35,14 @@ public enum MongoOperationType { CREATE_COLLECTION("createCollection", CreateCollectionOperator::new), CREATE_INDEX("createIndex", CreateIndexOperator::new), - INSERT("insert", InsertOperator::new); + INSERT("insert", InsertOperator::new), + DELETE("delete", DeleteOperator::new), + DROP_COLLECTION("dropCollection", DropCollectionOperator::new), + DROP_INDEX("dropIndex", DropIndexOperator::new), + RENAME_COLLECTION("renameCollection", RenameCollectionOperator::new), + MODIFY_COLLECTION("modifyCollection", ModifyCollectionOperator::new), + CREATE_VIEW("createView", CreateViewOperator::new), + DROP_VIEW("dropView", DropViewOperator::new); private final String value; private final BiFunction createOperatorFunction; diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/CreateViewOperator.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/CreateViewOperator.java new file mode 100644 index 000000000..09633971d --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/CreateViewOperator.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 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.template.mongodb.model.operator; + +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.CreateViewOptions; +import io.flamingock.template.mongodb.mapper.CreateViewOptionsMapper; +import io.flamingock.template.mongodb.model.MongoOperation; + +public class CreateViewOperator extends MongoOperator { + + public CreateViewOperator(MongoDatabase mongoDatabase, MongoOperation operation) { + super(mongoDatabase, operation, false); + } + + @Override + protected void applyInternal(ClientSession clientSession) { + CreateViewOptions options = CreateViewOptionsMapper.map(op.getOptions()); + mongoDatabase.createView(op.getCollection(), op.getViewOn(), op.getPipeline(), options); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/DeleteOperator.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/DeleteOperator.java new file mode 100644 index 000000000..0710038b9 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/DeleteOperator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 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.template.mongodb.model.operator; + +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; +import org.bson.Document; + +public class DeleteOperator extends MongoOperator { + + public DeleteOperator(MongoDatabase mongoDatabase, MongoOperation operation) { + super(mongoDatabase, operation, true); + } + + @Override + protected void applyInternal(ClientSession clientSession) { + MongoCollection collection = mongoDatabase.getCollection(op.getCollection()); + Document filter = op.getFilter(); + + if (clientSession != null) { + collection.deleteMany(clientSession, filter); + } else { + collection.deleteMany(filter); + } + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/DropCollectionOperator.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/DropCollectionOperator.java new file mode 100644 index 000000000..e27f59363 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/DropCollectionOperator.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 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.template.mongodb.model.operator; + +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; + +public class DropCollectionOperator extends MongoOperator { + + public DropCollectionOperator(MongoDatabase mongoDatabase, MongoOperation operation) { + super(mongoDatabase, operation, false); + } + + @Override + protected void applyInternal(ClientSession clientSession) { + mongoDatabase.getCollection(op.getCollection()).drop(); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/DropIndexOperator.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/DropIndexOperator.java new file mode 100644 index 000000000..504de9889 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/DropIndexOperator.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 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.template.mongodb.model.operator; + +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; + +public class DropIndexOperator extends MongoOperator { + + public DropIndexOperator(MongoDatabase mongoDatabase, MongoOperation operation) { + super(mongoDatabase, operation, false); + } + + @Override + protected void applyInternal(ClientSession clientSession) { + String indexName = op.getIndexName(); + if (indexName != null) { + mongoDatabase.getCollection(op.getCollection()).dropIndex(indexName); + } else { + mongoDatabase.getCollection(op.getCollection()).dropIndex(op.getKeys()); + } + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/DropViewOperator.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/DropViewOperator.java new file mode 100644 index 000000000..369bc9b2d --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/DropViewOperator.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 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.template.mongodb.model.operator; + +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; + +public class DropViewOperator extends MongoOperator { + + public DropViewOperator(MongoDatabase mongoDatabase, MongoOperation operation) { + super(mongoDatabase, operation, false); + } + + @Override + protected void applyInternal(ClientSession clientSession) { + mongoDatabase.getCollection(op.getCollection()).drop(); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/ModifyCollectionOperator.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/ModifyCollectionOperator.java new file mode 100644 index 000000000..fb0323ee2 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/ModifyCollectionOperator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 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.template.mongodb.model.operator; + +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; +import org.bson.Document; + +public class ModifyCollectionOperator extends MongoOperator { + + public ModifyCollectionOperator(MongoDatabase mongoDatabase, MongoOperation operation) { + super(mongoDatabase, operation, false); + } + + @Override + protected void applyInternal(ClientSession clientSession) { + Document command = new Document("collMod", op.getCollection()); + if (op.getValidator() != null) { + command.append("validator", op.getValidator()); + } + if (op.getValidationLevel() != null) { + command.append("validationLevel", op.getValidationLevel()); + } + if (op.getValidationAction() != null) { + command.append("validationAction", op.getValidationAction()); + } + mongoDatabase.runCommand(command); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/RenameCollectionOperator.java b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/RenameCollectionOperator.java new file mode 100644 index 000000000..af50238a6 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/main/java/io/flamingock/template/mongodb/model/operator/RenameCollectionOperator.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 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.template.mongodb.model.operator; + +import com.mongodb.MongoNamespace; +import com.mongodb.client.ClientSession; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.RenameCollectionOptions; +import io.flamingock.template.mongodb.mapper.RenameCollectionOptionsMapper; +import io.flamingock.template.mongodb.model.MongoOperation; + +public class RenameCollectionOperator extends MongoOperator { + + public RenameCollectionOperator(MongoDatabase mongoDatabase, MongoOperation operation) { + super(mongoDatabase, operation, false); + } + + @Override + protected void applyInternal(ClientSession clientSession) { + MongoNamespace target = new MongoNamespace(mongoDatabase.getName(), op.getTarget()); + RenameCollectionOptions options = RenameCollectionOptionsMapper.map(op.getOptions()); + mongoDatabase.getCollection(op.getCollection()).renameCollection(target, options); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/AutoRollbackTemplateTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/AutoRollbackTemplateTest.java new file mode 100644 index 000000000..d0e4b2e2b --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/AutoRollbackTemplateTest.java @@ -0,0 +1,145 @@ +/* + * Copyright 2025 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.template.mongodb; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoApplyPayload; +import io.flamingock.template.mongodb.model.MongoOperation; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Template-level test for auto-rollback behavior in MongoChangeTemplate. + * + *

This test verifies that when an operation fails during template execution, + * the auto-rollback mechanism correctly rolls back previously successful + * operations in reverse order.

+ * + */ +@Testcontainers +class AutoRollbackTemplateTest { + + private static final String DB_NAME = "test"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection("auto_rollback_test").drop(); + } + + @Test + @DisplayName("WHEN operation fails during template execution THEN auto-rollback is triggered for previous operations") + void autoRollbackOnFailureTest() { + assertFalse(collectionExists("auto_rollback_test"), "Collection should not exist before test"); + + MongoApplyPayload payload = new MongoApplyPayload(); + List ops = new ArrayList<>(); + + MongoOperation op1 = new MongoOperation(); + op1.setType("createCollection"); + op1.setCollection("auto_rollback_test"); + op1.setParameters(new HashMap<>()); + MongoOperation rollback1 = new MongoOperation(); + rollback1.setType("dropCollection"); + rollback1.setCollection("auto_rollback_test"); + rollback1.setParameters(new HashMap<>()); + op1.setRollback(rollback1); + ops.add(op1); + + MongoOperation op2 = new MongoOperation(); + op2.setType("insert"); + op2.setCollection("auto_rollback_test"); + Map insertParams = new HashMap<>(); + List> docs = new ArrayList<>(); + Map doc1 = new HashMap<>(); + doc1.put("name", "Test User 1"); + docs.add(doc1); + Map doc2 = new HashMap<>(); + doc2.put("name", "Test User 2"); + docs.add(doc2); + insertParams.put("documents", docs); + op2.setParameters(insertParams); + MongoOperation rollback2 = new MongoOperation(); + rollback2.setType("delete"); + rollback2.setCollection("auto_rollback_test"); + Map deleteParams = new HashMap<>(); + deleteParams.put("filter", new HashMap<>()); + rollback2.setParameters(deleteParams); + op2.setRollback(rollback2); + ops.add(op2); + + MongoOperation op3 = new MongoOperation(); + op3.setType("createCollection"); + op3.setCollection("auto_rollback_test"); + op3.setParameters(new HashMap<>()); + ops.add(op3); + + payload.setOperations(ops); + + MongoChangeTemplate template = new MongoChangeTemplate(); + template.setApplyPayload(payload); + + Exception thrown = assertThrows(Exception.class, () -> { + template.apply(mongoDatabase, null); + }); + + assertTrue(thrown.getMessage().contains("already exists") || + thrown.getCause().getMessage().contains("already exists"), + "Should fail with 'already exists' error"); + + assertFalse(collectionExists("auto_rollback_test"), + "Collection should not exist after auto-rollback (dropCollection rollback executed)"); + } + + private boolean collectionExists(String collectionName) { + return mongoDatabase.listCollectionNames() + .into(new ArrayList<>()) + .contains(collectionName); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java index e46431f3c..9fa5cdff6 100644 --- a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java @@ -40,6 +40,7 @@ import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; @EnableFlamingock(configFile = "flamingock/pipeline.yaml") @Testcontainers @@ -68,7 +69,9 @@ static void beforeAll() { @BeforeEach void setupEach() { mongoDatabase.getCollection(DEFAULT_AUDIT_STORE_NAME).drop(); - mongoDatabase.getCollection(DEFAULT_AUDIT_STORE_NAME).drop(); + mongoDatabase.getCollection("users").drop(); + mongoDatabase.getCollection("products").drop(); + mongoDatabase.getCollection("orders").drop(); } @@ -88,7 +91,7 @@ void happyPath() { .find() .into(new ArrayList<>()); - assertEquals(4, auditLog.size()); + assertEquals(8, auditLog.size()); assertEquals("create-users-collection-with-index", auditLog.get(0).getString("changeId")); assertEquals(AuditEntry.Status.STARTED.name(), auditLog.get(0).getString("state")); @@ -100,6 +103,17 @@ void happyPath() { assertEquals("seed-users", auditLog.get(3).getString("changeId")); assertEquals(AuditEntry.Status.APPLIED.name(), auditLog.get(3).getString("state")); + assertEquals("multiple-operations-change", auditLog.get(4).getString("changeId")); + assertEquals(AuditEntry.Status.STARTED.name(), auditLog.get(4).getString("state")); + assertEquals("multiple-operations-change", auditLog.get(5).getString("changeId")); + assertEquals(AuditEntry.Status.APPLIED.name(), auditLog.get(5).getString("state")); + + assertEquals("per-operation-rollback-change", auditLog.get(6).getString("changeId")); + assertEquals(AuditEntry.Status.STARTED.name(), auditLog.get(6).getString("state")); + assertEquals("per-operation-rollback-change", auditLog.get(7).getString("changeId")); + assertEquals(AuditEntry.Status.APPLIED.name(), auditLog.get(7).getString("state")); + + // Verify for single operation List users = mongoDatabase.getCollection("users") .find() .into(new ArrayList<>()); @@ -112,7 +126,40 @@ void happyPath() { assertEquals("Backup", users.get(1).getString("name")); assertEquals("backup@company.com", users.get(1).getString("email")); assertEquals("readonly", users.get(1).getList("roles", String.class).get(0)); - } + // Verify for multiple operation + List products = mongoDatabase.getCollection("products") + .find() + .into(new ArrayList<>()); + + assertEquals(3, products.size(), "Should have 3 products from multiple operations"); + assertEquals("Laptop", products.get(0).getString("name")); + assertEquals("Keyboard", products.get(1).getString("name")); + assertEquals("Mouse", products.get(2).getString("name")); + + List indexes = mongoDatabase.getCollection("products") + .listIndexes() + .into(new ArrayList<>()); + boolean categoryIndexExists = indexes.stream() + .anyMatch(idx -> "category_index".equals(idx.getString("name"))); + assertTrue(categoryIndexExists, "Category index should exist on products collection"); + + List orders = mongoDatabase.getCollection("orders") + .find() + .into(new ArrayList<>()); + + assertEquals(2, orders.size(), "Should have 2 orders from per-operation rollback change"); + assertEquals("ORD-001", orders.get(0).getString("orderId")); + assertEquals("John Doe", orders.get(0).getString("customer")); + assertEquals("ORD-002", orders.get(1).getString("orderId")); + assertEquals("Jane Smith", orders.get(1).getString("customer")); + + List orderIndexes = mongoDatabase.getCollection("orders") + .listIndexes() + .into(new ArrayList<>()); + boolean orderIdIndexExists = orderIndexes.stream() + .anyMatch(idx -> "orderId_index".equals(idx.getString("name"))); + assertTrue(orderIdIndexExists, "orderId_index should exist on orders collection"); + } } diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/changes/_0003__multiple_operations.yaml b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/changes/_0003__multiple_operations.yaml new file mode 100644 index 000000000..e1627b83e --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/changes/_0003__multiple_operations.yaml @@ -0,0 +1,31 @@ +id: multiple-operations-change +transactional: false +template: MongoChangeTemplate +targetSystem: + id: "mongodb" +apply: + operations: + - type: createCollection + collection: products + + - type: insert + collection: products + parameters: + documents: + - name: "Laptop" + price: 999.99 + category: "Electronics" + - name: "Keyboard" + price: 79.99 + category: "Electronics" + - name: "Mouse" + price: 29.99 + category: "Electronics" + + - type: createIndex + collection: products + parameters: + keys: + category: 1 + options: + name: "category_index" diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/changes/_0004__per_operation_rollback.yaml b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/changes/_0004__per_operation_rollback.yaml new file mode 100644 index 000000000..c68f446ba --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/changes/_0004__per_operation_rollback.yaml @@ -0,0 +1,42 @@ +id: per-operation-rollback-change +transactional: false +template: MongoChangeTemplate +targetSystem: + id: "mongodb" +apply: + operations: + - type: createCollection + collection: orders + rollback: + type: dropCollection + collection: orders + + - type: insert + collection: orders + parameters: + documents: + - orderId: "ORD-001" + customer: "John Doe" + total: 150.00 + - orderId: "ORD-002" + customer: "Jane Smith" + total: 250.00 + rollback: + type: delete + collection: orders + parameters: + filter: {} + + - type: createIndex + collection: orders + parameters: + keys: + orderId: 1 + options: + name: "orderId_index" + unique: true + rollback: + type: dropIndex + collection: orders + parameters: + indexName: "orderId_index" diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/CreateCollectionOperatorTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/CreateCollectionOperatorTest.java new file mode 100644 index 000000000..4aa050b11 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/CreateCollectionOperatorTest.java @@ -0,0 +1,87 @@ +/* + * Copyright 2025 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.template.mongodb.operations; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; +import io.flamingock.template.mongodb.model.operator.CreateCollectionOperator; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +class CreateCollectionOperatorTest { + + private static final String DB_NAME = "test"; + private static final String COLLECTION_NAME = "newTestCollection"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection(COLLECTION_NAME).drop(); + } + + @Test + @DisplayName("WHEN createCollection operator is applied THEN collection is created") + void createCollectionTest() { + assertFalse(collectionExists(COLLECTION_NAME), "Collection should not exist before creation"); + + MongoOperation operation = new MongoOperation(); + operation.setType("createCollection"); + operation.setCollection(COLLECTION_NAME); + operation.setParameters(new HashMap<>()); + + CreateCollectionOperator operator = new CreateCollectionOperator(mongoDatabase, operation); + operator.apply(null); + + assertTrue(collectionExists(COLLECTION_NAME), "Collection should exist after creation"); + } + + private boolean collectionExists(String collectionName) { + return mongoDatabase.listCollectionNames() + .into(new ArrayList<>()) + .contains(collectionName); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/CreateIndexOperatorTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/CreateIndexOperatorTest.java new file mode 100644 index 000000000..a49ae801e --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/CreateIndexOperatorTest.java @@ -0,0 +1,143 @@ +/* + * Copyright 2025 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.template.mongodb.operations; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; +import io.flamingock.template.mongodb.model.operator.CreateIndexOperator; +import org.bson.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +class CreateIndexOperatorTest { + + private static final String DB_NAME = "test"; + private static final String COLLECTION_NAME = "indexTestCollection"; + private static final String INDEX_NAME = "email_unique_index"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection(COLLECTION_NAME).drop(); + } + + @Test + @DisplayName("WHEN createIndex operator is applied THEN index is created") + void createIndexTest() { + mongoDatabase.createCollection(COLLECTION_NAME); + + MongoOperation operation = new MongoOperation(); + operation.setType("createIndex"); + operation.setCollection(COLLECTION_NAME); + + Map params = new HashMap<>(); + Map keys = new HashMap<>(); + keys.put("email", 1); + params.put("keys", keys); + operation.setParameters(params); + + CreateIndexOperator operator = new CreateIndexOperator(mongoDatabase, operation); + operator.apply(null); + + assertTrue(indexExistsByKeys("email"), "Index should exist after creation"); + } + + @Test + @DisplayName("WHEN createIndex operator is applied with options THEN index is created with options") + void createIndexWithOptionsTest() { + mongoDatabase.createCollection(COLLECTION_NAME); + + MongoOperation operation = new MongoOperation(); + operation.setType("createIndex"); + operation.setCollection(COLLECTION_NAME); + + Map params = new HashMap<>(); + Map keys = new HashMap<>(); + keys.put("email", 1); + params.put("keys", keys); + + Map options = new HashMap<>(); + options.put("unique", true); + options.put("name", INDEX_NAME); + params.put("options", options); + + operation.setParameters(params); + + CreateIndexOperator operator = new CreateIndexOperator(mongoDatabase, operation); + operator.apply(null); + + assertTrue(indexExists(INDEX_NAME), "Index should exist with specified name"); + assertTrue(isIndexUnique(INDEX_NAME), "Index should be unique"); + } + + private boolean indexExists(String indexName) { + List indexes = mongoDatabase.getCollection(COLLECTION_NAME) + .listIndexes() + .into(new ArrayList<>()); + return indexes.stream().anyMatch(idx -> indexName.equals(idx.getString("name"))); + } + + private boolean indexExistsByKeys(String keyField) { + List indexes = mongoDatabase.getCollection(COLLECTION_NAME) + .listIndexes() + .into(new ArrayList<>()); + return indexes.stream().anyMatch(idx -> { + Document key = idx.get("key", Document.class); + return key != null && key.containsKey(keyField); + }); + } + + private boolean isIndexUnique(String indexName) { + List indexes = mongoDatabase.getCollection(COLLECTION_NAME) + .listIndexes() + .into(new ArrayList<>()); + return indexes.stream() + .filter(idx -> indexName.equals(idx.getString("name"))) + .anyMatch(idx -> Boolean.TRUE.equals(idx.getBoolean("unique"))); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/CreateViewOperatorTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/CreateViewOperatorTest.java new file mode 100644 index 000000000..b8b3330c4 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/CreateViewOperatorTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2025 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.template.mongodb.operations; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; +import io.flamingock.template.mongodb.model.operator.CreateViewOperator; +import org.bson.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +class CreateViewOperatorTest { + + private static final String DB_NAME = "test"; + private static final String SOURCE_COLLECTION = "viewSourceCollection"; + private static final String VIEW_NAME = "activeUsersView"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection(SOURCE_COLLECTION).drop(); + mongoDatabase.getCollection(VIEW_NAME).drop(); + } + + @Test + @DisplayName("WHEN createView operator is applied THEN view is created and filters data") + void createViewTest() { + mongoDatabase.createCollection(SOURCE_COLLECTION); + mongoDatabase.getCollection(SOURCE_COLLECTION).insertMany(Arrays.asList( + new Document("name", "Active User").append("status", "active"), + new Document("name", "Inactive User").append("status", "inactive") + )); + + MongoOperation operation = new MongoOperation(); + operation.setType("createView"); + operation.setCollection(VIEW_NAME); + + Map params = new HashMap<>(); + params.put("viewOn", SOURCE_COLLECTION); + + List> pipeline = new ArrayList<>(); + Map matchStage = new HashMap<>(); + Map matchQuery = new HashMap<>(); + matchQuery.put("status", "active"); + matchStage.put("$match", matchQuery); + pipeline.add(matchStage); + params.put("pipeline", pipeline); + + operation.setParameters(params); + + CreateViewOperator operator = new CreateViewOperator(mongoDatabase, operation); + operator.apply(null); + + List collections = mongoDatabase.listCollectionNames().into(new ArrayList<>()); + assertTrue(collections.contains(VIEW_NAME), "View should exist"); + + List viewResults = mongoDatabase.getCollection(VIEW_NAME) + .find() + .into(new ArrayList<>()); + assertEquals(1, viewResults.size(), "View should return only active users"); + assertEquals("Active User", viewResults.get(0).getString("name")); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/DeleteOperatorTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/DeleteOperatorTest.java new file mode 100644 index 000000000..c72cda2fa --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/DeleteOperatorTest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2025 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.template.mongodb.operations; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; +import io.flamingock.template.mongodb.model.operator.DeleteOperator; +import org.bson.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Testcontainers +class DeleteOperatorTest { + + private static final String DB_NAME = "test"; + private static final String COLLECTION_NAME = "deleteTestCollection"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection(COLLECTION_NAME).drop(); + mongoDatabase.createCollection(COLLECTION_NAME); + List docs = new ArrayList<>(); + docs.add(new Document("name", "Alice").append("role", "admin")); + docs.add(new Document("name", "Bob").append("role", "user")); + docs.add(new Document("name", "Charlie").append("role", "user")); + docs.add(new Document("name", "Diana").append("role", "admin")); + mongoDatabase.getCollection(COLLECTION_NAME).insertMany(docs); + } + + @Test + @DisplayName("WHEN delete operator is applied with specific filter THEN matching documents are deleted") + void deleteWithFilterTest() { + assertEquals(4, getDocumentCount(), "Collection should have 4 documents before delete"); + + MongoOperation operation = new MongoOperation(); + operation.setType("delete"); + operation.setCollection(COLLECTION_NAME); + + Map params = new HashMap<>(); + Map filter = new HashMap<>(); + filter.put("role", "user"); + params.put("filter", filter); + operation.setParameters(params); + + DeleteOperator operator = new DeleteOperator(mongoDatabase, operation); + operator.apply(null); + + assertEquals(2, getDocumentCount(), "Collection should have 2 documents after delete"); + + List remainingDocs = mongoDatabase.getCollection(COLLECTION_NAME) + .find() + .into(new ArrayList<>()); + + for (Document doc : remainingDocs) { + assertEquals("admin", doc.getString("role"), "Only admin documents should remain"); + } + } + + @Test + @DisplayName("WHEN delete operator is applied with empty filter THEN all documents are deleted") + void deleteAllWithEmptyFilterTest() { + assertEquals(4, getDocumentCount(), "Collection should have 4 documents before delete"); + + MongoOperation operation = new MongoOperation(); + operation.setType("delete"); + operation.setCollection(COLLECTION_NAME); + + Map params = new HashMap<>(); + params.put("filter", new HashMap<>()); + operation.setParameters(params); + + DeleteOperator operator = new DeleteOperator(mongoDatabase, operation); + operator.apply(null); + + assertEquals(0, getDocumentCount(), "Collection should be empty after delete with empty filter"); + } + + @Test + @DisplayName("WHEN delete operator is applied with filter matching single document THEN only that document is deleted") + void deleteSingleDocumentTest() { + assertEquals(4, getDocumentCount(), "Collection should have 4 documents before delete"); + + MongoOperation operation = new MongoOperation(); + operation.setType("delete"); + operation.setCollection(COLLECTION_NAME); + + Map params = new HashMap<>(); + Map filter = new HashMap<>(); + filter.put("name", "Alice"); + params.put("filter", filter); + operation.setParameters(params); + + DeleteOperator operator = new DeleteOperator(mongoDatabase, operation); + operator.apply(null); + + assertEquals(3, getDocumentCount(), "Collection should have 3 documents after delete"); + + Document alice = mongoDatabase.getCollection(COLLECTION_NAME) + .find(new Document("name", "Alice")) + .first(); + assertNull(alice, "Alice should be deleted"); + } + + @Test + @DisplayName("WHEN delete operator is applied with non-matching filter THEN no documents are deleted") + void deleteWithNonMatchingFilterTest() { + assertEquals(4, getDocumentCount(), "Collection should have 4 documents before delete"); + + MongoOperation operation = new MongoOperation(); + operation.setType("delete"); + operation.setCollection(COLLECTION_NAME); + + Map params = new HashMap<>(); + Map filter = new HashMap<>(); + filter.put("name", "NonExistent"); + params.put("filter", filter); + operation.setParameters(params); + + DeleteOperator operator = new DeleteOperator(mongoDatabase, operation); + operator.apply(null); + + assertEquals(4, getDocumentCount(), "Collection should still have 4 documents"); + } + + private long getDocumentCount() { + return mongoDatabase.getCollection(COLLECTION_NAME).countDocuments(); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/DropCollectionOperatorTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/DropCollectionOperatorTest.java new file mode 100644 index 000000000..276f49e52 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/DropCollectionOperatorTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2025 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.template.mongodb.operations; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; +import io.flamingock.template.mongodb.model.operator.DropCollectionOperator; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +class DropCollectionOperatorTest { + + private static final String DB_NAME = "test"; + private static final String COLLECTION_NAME = "testCollection"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection(COLLECTION_NAME).drop(); + } + + @Test + @DisplayName("WHEN dropCollection operator is applied THEN collection is dropped") + void dropCollectionTest() { + mongoDatabase.createCollection(COLLECTION_NAME); + assertTrue(collectionExists(COLLECTION_NAME), "Collection should exist before drop"); + + MongoOperation operation = new MongoOperation(); + operation.setType("dropCollection"); + operation.setCollection(COLLECTION_NAME); + operation.setParameters(new HashMap<>()); + + DropCollectionOperator operator = new DropCollectionOperator(mongoDatabase, operation); + operator.apply(null); + + assertFalse(collectionExists(COLLECTION_NAME), "Collection should have been dropped"); + } + + private boolean collectionExists(String collectionName) { + return mongoDatabase.listCollectionNames() + .into(new ArrayList<>()) + .contains(collectionName); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/DropIndexOperatorTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/DropIndexOperatorTest.java new file mode 100644 index 000000000..fbb4a3342 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/DropIndexOperatorTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2025 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.template.mongodb.operations; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; +import io.flamingock.template.mongodb.model.operator.DropIndexOperator; +import org.bson.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +class DropIndexOperatorTest { + + private static final String DB_NAME = "test"; + private static final String COLLECTION_NAME = "indexTestCollection"; + private static final String INDEX_NAME = "email_index"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection(COLLECTION_NAME).drop(); + } + + @Test + @DisplayName("WHEN dropIndex operator is applied with index name THEN index is dropped") + void dropIndexByNameTest() { + mongoDatabase.createCollection(COLLECTION_NAME); + mongoDatabase.getCollection(COLLECTION_NAME).createIndex(new Document("email", 1), + new com.mongodb.client.model.IndexOptions().name(INDEX_NAME)); + assertTrue(indexExists(INDEX_NAME), "Index should exist before drop"); + + MongoOperation operation = new MongoOperation(); + operation.setType("dropIndex"); + operation.setCollection(COLLECTION_NAME); + Map params = new HashMap<>(); + params.put("indexName", INDEX_NAME); + operation.setParameters(params); + + DropIndexOperator operator = new DropIndexOperator(mongoDatabase, operation); + operator.apply(null); + + assertFalse(indexExists(INDEX_NAME), "Index should have been dropped"); + } + + @Test + @DisplayName("WHEN dropIndex operator is applied with keys THEN index is dropped") + void dropIndexByKeysTest() { + mongoDatabase.createCollection(COLLECTION_NAME); + mongoDatabase.getCollection(COLLECTION_NAME).createIndex(new Document("name", 1)); + assertTrue(indexExistsByKeys("name"), "Index should exist before drop"); + + MongoOperation operation = new MongoOperation(); + operation.setType("dropIndex"); + operation.setCollection(COLLECTION_NAME); + Map params = new HashMap<>(); + Map keys = new HashMap<>(); + keys.put("name", 1); + params.put("keys", keys); + operation.setParameters(params); + + DropIndexOperator operator = new DropIndexOperator(mongoDatabase, operation); + operator.apply(null); + + assertFalse(indexExistsByKeys("name"), "Index should have been dropped"); + } + + private boolean indexExists(String indexName) { + List indexes = mongoDatabase.getCollection(COLLECTION_NAME) + .listIndexes() + .into(new ArrayList<>()); + return indexes.stream().anyMatch(idx -> indexName.equals(idx.getString("name"))); + } + + private boolean indexExistsByKeys(String keyField) { + List indexes = mongoDatabase.getCollection(COLLECTION_NAME) + .listIndexes() + .into(new ArrayList<>()); + return indexes.stream().anyMatch(idx -> { + Document key = idx.get("key", Document.class); + return key != null && key.containsKey(keyField); + }); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/DropViewOperatorTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/DropViewOperatorTest.java new file mode 100644 index 000000000..dfd2b1e4a --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/DropViewOperatorTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025 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.template.mongodb.operations; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; +import io.flamingock.template.mongodb.model.operator.DropViewOperator; +import org.bson.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +class DropViewOperatorTest { + + private static final String DB_NAME = "test"; + private static final String SOURCE_COLLECTION = "dropViewSourceCollection"; + private static final String VIEW_NAME = "viewToDrop"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection(SOURCE_COLLECTION).drop(); + mongoDatabase.getCollection(VIEW_NAME).drop(); + } + + @Test + @DisplayName("WHEN dropView operator is applied THEN view is dropped") + void dropViewTest() { + mongoDatabase.createCollection(SOURCE_COLLECTION); + mongoDatabase.createView(VIEW_NAME, SOURCE_COLLECTION, Collections.emptyList()); + assertTrue(collectionExists(VIEW_NAME), "View should exist before drop"); + + MongoOperation operation = new MongoOperation(); + operation.setType("dropView"); + operation.setCollection(VIEW_NAME); + operation.setParameters(new HashMap<>()); + + DropViewOperator operator = new DropViewOperator(mongoDatabase, operation); + operator.apply(null); + + assertFalse(collectionExists(VIEW_NAME), "View should have been dropped"); + } + + private boolean collectionExists(String collectionName) { + List collections = mongoDatabase.listCollectionNames().into(new ArrayList<>()); + return collections.contains(collectionName); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/InsertOperatorTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/InsertOperatorTest.java new file mode 100644 index 000000000..87334c1a6 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/InsertOperatorTest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2025 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.template.mongodb.operations; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; +import io.flamingock.template.mongodb.model.operator.InsertOperator; +import org.bson.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Testcontainers +class InsertOperatorTest { + + private static final String DB_NAME = "test"; + private static final String COLLECTION_NAME = "insertTestCollection"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection(COLLECTION_NAME).drop(); + mongoDatabase.createCollection(COLLECTION_NAME); + } + + @Test + @DisplayName("WHEN insert operator is applied with one document THEN document is inserted") + void insertOneDocumentTest() { + // Verify collection is empty before + assertEquals(0, getDocumentCount(), "Collection should be empty before insert"); + + // Create the operation + MongoOperation operation = new MongoOperation(); + operation.setType("insert"); + operation.setCollection(COLLECTION_NAME); + + Map params = new HashMap<>(); + List> documents = new ArrayList<>(); + Map doc = new HashMap<>(); + doc.put("name", "John Doe"); + doc.put("email", "john@example.com"); + documents.add(doc); + params.put("documents", documents); + operation.setParameters(params); + + // Execute the operator + InsertOperator operator = new InsertOperator(mongoDatabase, operation); + operator.apply(null); + + // Verify + assertEquals(1, getDocumentCount(), "Collection should have one document"); + Document inserted = mongoDatabase.getCollection(COLLECTION_NAME).find().first(); + assertEquals("John Doe", inserted.getString("name")); + assertEquals("john@example.com", inserted.getString("email")); + } + + @Test + @DisplayName("WHEN insert operator is applied with multiple documents THEN documents are inserted") + void insertManyDocumentsTest() { + assertEquals(0, getDocumentCount(), "Collection should be empty before insert"); + + MongoOperation operation = new MongoOperation(); + operation.setType("insert"); + operation.setCollection(COLLECTION_NAME); + + Map params = new HashMap<>(); + List> documents = new ArrayList<>(); + + Map doc1 = new HashMap<>(); + doc1.put("name", "Alice"); + doc1.put("email", "alice@example.com"); + documents.add(doc1); + + Map doc2 = new HashMap<>(); + doc2.put("name", "Bob"); + doc2.put("email", "bob@example.com"); + documents.add(doc2); + + Map doc3 = new HashMap<>(); + doc3.put("name", "Charlie"); + doc3.put("email", "charlie@example.com"); + documents.add(doc3); + + params.put("documents", documents); + operation.setParameters(params); + + InsertOperator operator = new InsertOperator(mongoDatabase, operation); + operator.apply(null); + + assertEquals(3, getDocumentCount(), "Collection should have three documents"); + + List insertedDocs = mongoDatabase.getCollection(COLLECTION_NAME) + .find() + .into(new ArrayList<>()); + + List names = Arrays.asList("Alice", "Bob", "Charlie"); + for (Document doc : insertedDocs) { + assertTrue(names.contains(doc.getString("name")), "Document name should be in expected list"); + } + } + + @Test + @DisplayName("WHEN insert operator is applied with empty documents THEN nothing is inserted") + void insertEmptyDocumentsTest() { + assertEquals(0, getDocumentCount(), "Collection should be empty before insert"); + + MongoOperation operation = new MongoOperation(); + operation.setType("insert"); + operation.setCollection(COLLECTION_NAME); + + Map params = new HashMap<>(); + params.put("documents", new ArrayList<>()); + operation.setParameters(params); + + InsertOperator operator = new InsertOperator(mongoDatabase, operation); + operator.apply(null); + + assertEquals(0, getDocumentCount(), "Collection should still be empty"); + } + + private long getDocumentCount() { + return mongoDatabase.getCollection(COLLECTION_NAME).countDocuments(); + } + + private static void assertTrue(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/ModifyCollectionOperatorTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/ModifyCollectionOperatorTest.java new file mode 100644 index 000000000..0a4536941 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/ModifyCollectionOperatorTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2025 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.template.mongodb.operations; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; +import io.flamingock.template.mongodb.model.operator.ModifyCollectionOperator; +import org.bson.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Testcontainers +class ModifyCollectionOperatorTest { + + private static final String DB_NAME = "test"; + private static final String COLLECTION_NAME = "modifyTestCollection"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection(COLLECTION_NAME).drop(); + } + + @Test + @DisplayName("WHEN modifyCollection operator is applied THEN collection validation is set") + void modifyCollectionTest() { + mongoDatabase.createCollection(COLLECTION_NAME); + + MongoOperation operation = new MongoOperation(); + operation.setType("modifyCollection"); + operation.setCollection(COLLECTION_NAME); + + Map params = new HashMap<>(); + Map validator = new HashMap<>(); + Map jsonSchema = new HashMap<>(); + jsonSchema.put("bsonType", "object"); + validator.put("$jsonSchema", jsonSchema); + params.put("validator", validator); + params.put("validationLevel", "moderate"); + params.put("validationAction", "warn"); + operation.setParameters(params); + + ModifyCollectionOperator operator = new ModifyCollectionOperator(mongoDatabase, operation); + operator.apply(null); + + Document collectionInfo = mongoDatabase.listCollections() + .filter(new Document("name", COLLECTION_NAME)) + .first(); + assertNotNull(collectionInfo, "Collection should exist"); + Document options = collectionInfo.get("options", Document.class); + assertNotNull(options, "Collection should have options"); + assertNotNull(options.get("validator"), "Collection should have validator"); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/MultipleOperationsTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/MultipleOperationsTest.java new file mode 100644 index 000000000..b37a733a2 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/MultipleOperationsTest.java @@ -0,0 +1,196 @@ +/* + * Copyright 2025 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.template.mongodb.operations; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoApplyPayload; +import io.flamingock.template.mongodb.model.MongoOperation; +import org.bson.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +class MultipleOperationsTest { + + private static final String DB_NAME = "test"; + private static final String COLLECTION_NAME = "multiOpsCollection"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection(COLLECTION_NAME).drop(); + } + + @Test + @DisplayName("WHEN multiple operations are executed THEN all operations complete successfully") + void multipleOperationsTest() { + MongoApplyPayload payload = new MongoApplyPayload(); + List ops = new ArrayList<>(); + + // Op 1: Create collection + MongoOperation op1 = new MongoOperation(); + op1.setType("createCollection"); + op1.setCollection(COLLECTION_NAME); + op1.setParameters(new HashMap<>()); + ops.add(op1); + + // Op 2: Insert documents + MongoOperation op2 = new MongoOperation(); + op2.setType("insert"); + op2.setCollection(COLLECTION_NAME); + Map insertParams = new HashMap<>(); + List> documents = new ArrayList<>(); + Map doc1 = new HashMap<>(); + doc1.put("name", "User1"); + doc1.put("email", "user1@example.com"); + documents.add(doc1); + Map doc2 = new HashMap<>(); + doc2.put("name", "User2"); + doc2.put("email", "user2@example.com"); + documents.add(doc2); + insertParams.put("documents", documents); + op2.setParameters(insertParams); + ops.add(op2); + + // Op 3: Create index + MongoOperation op3 = new MongoOperation(); + op3.setType("createIndex"); + op3.setCollection(COLLECTION_NAME); + Map indexParams = new HashMap<>(); + Map keys = new HashMap<>(); + keys.put("email", 1); + indexParams.put("keys", keys); + Map options = new HashMap<>(); + options.put("unique", true); + indexParams.put("options", options); + op3.setParameters(indexParams); + ops.add(op3); + + payload.setOperations(ops); + + for (MongoOperation op : payload.getOperations()) { + op.getOperator(mongoDatabase).apply(null); + } + + assertTrue(collectionExists(COLLECTION_NAME), "Collection should exist"); + assertEquals(2, getDocumentCount(), "Should have 2 documents"); + assertTrue(indexExists("email"), "Index on email should exist"); + } + + @Test + @DisplayName("WHEN single operation format is used THEN backward compatibility works") + void backwardCompatibilitySingleOperationTest() { + MongoApplyPayload payload = new MongoApplyPayload(); + payload.setType("createCollection"); + payload.setCollection(COLLECTION_NAME); + payload.setParameters(new HashMap<>()); + + List ops = payload.getOperations(); + assertEquals(1, ops.size(), "Should have exactly one operation"); + assertEquals("createCollection", ops.get(0).getType()); + assertEquals(COLLECTION_NAME, ops.get(0).getCollection()); + + for (MongoOperation op : ops) { + op.getOperator(mongoDatabase).apply(null); + } + + assertTrue(collectionExists(COLLECTION_NAME), "Collection should exist"); + } + + @Test + @DisplayName("WHEN payload has no operations THEN empty list is returned") + void emptyPayloadTest() { + MongoApplyPayload payload = new MongoApplyPayload(); + + List ops = payload.getOperations(); + assertTrue(ops.isEmpty(), "Should return empty list"); + } + + @Test + @DisplayName("WHEN operations list is set but type is also set THEN operations list takes precedence") + void operationsListTakesPrecedenceTest() { + MongoApplyPayload payload = new MongoApplyPayload(); + + // Backward compatibility + payload.setType("dropCollection"); + payload.setCollection("someOtherCollection"); + + List ops = new ArrayList<>(); + MongoOperation op = new MongoOperation(); + op.setType("createCollection"); + op.setCollection(COLLECTION_NAME); + op.setParameters(new HashMap<>()); + ops.add(op); + payload.setOperations(ops); + + List result = payload.getOperations(); + assertEquals(1, result.size()); + assertEquals("createCollection", result.get(0).getType()); + assertEquals(COLLECTION_NAME, result.get(0).getCollection()); + } + + private boolean collectionExists(String collectionName) { + return mongoDatabase.listCollectionNames() + .into(new ArrayList<>()) + .contains(collectionName); + } + + private long getDocumentCount() { + return mongoDatabase.getCollection(COLLECTION_NAME).countDocuments(); + } + + private boolean indexExists(String keyField) { + List indexes = mongoDatabase.getCollection(COLLECTION_NAME) + .listIndexes() + .into(new ArrayList<>()); + return indexes.stream().anyMatch(idx -> { + Document key = idx.get("key", Document.class); + return key != null && key.containsKey(keyField); + }); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/PerOperationRollbackTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/PerOperationRollbackTest.java new file mode 100644 index 000000000..654526887 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/PerOperationRollbackTest.java @@ -0,0 +1,296 @@ +/* + * Copyright 2025 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.template.mongodb.operations; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoApplyPayload; +import io.flamingock.template.mongodb.model.MongoOperation; +import org.bson.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@Testcontainers +class PerOperationRollbackTest { + + private static final String DB_NAME = "test"; + private static final String COLLECTION_NAME = "rollbackTestCollection"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection(COLLECTION_NAME).drop(); + mongoDatabase.getCollection("failCollection").drop(); + } + + @Test + @DisplayName("WHEN all operations succeed THEN no rollback is triggered") + void successfulApplyNoRollbackTest() { + MongoApplyPayload payload = new MongoApplyPayload(); + List ops = new ArrayList<>(); + + MongoOperation op1 = createOperation("createCollection", COLLECTION_NAME); + op1.setRollback(createOperation("dropCollection", COLLECTION_NAME)); + ops.add(op1); + + MongoOperation op2 = createInsertOperation(COLLECTION_NAME, "TestUser"); + MongoOperation rollback2 = new MongoOperation(); + rollback2.setType("delete"); + rollback2.setCollection(COLLECTION_NAME); + Map deleteParams = new HashMap<>(); + deleteParams.put("filter", new HashMap<>()); + rollback2.setParameters(deleteParams); + op2.setRollback(rollback2); + ops.add(op2); + + payload.setOperations(ops); + + List successfulOps = new ArrayList<>(); + for (MongoOperation op : payload.getOperations()) { + op.getOperator(mongoDatabase).apply(null); + successfulOps.add(op); + } + + // All operations ok + assertTrue(collectionExists(COLLECTION_NAME), "Collection should exist"); + assertEquals(1, getDocumentCount(COLLECTION_NAME), "Should have 1 document"); + } + + @Test + @DisplayName("WHEN operation fails THEN previous operations are rolled back in reverse order") + void partialFailureAutoRollbackTest() { + mongoDatabase.createCollection(COLLECTION_NAME); + mongoDatabase.getCollection(COLLECTION_NAME).insertOne(new Document("name", "Existing")); + + MongoApplyPayload payload = new MongoApplyPayload(); + List ops = new ArrayList<>(); + + MongoOperation op1 = createInsertOperation(COLLECTION_NAME, "NewUser"); + MongoOperation rollback1 = new MongoOperation(); + rollback1.setType("delete"); + rollback1.setCollection(COLLECTION_NAME); + Map deleteParams = new HashMap<>(); + Map filter = new HashMap<>(); + filter.put("name", "NewUser"); + deleteParams.put("filter", filter); + rollback1.setParameters(deleteParams); + op1.setRollback(rollback1); + ops.add(op1); + + // Will fail, already exists + MongoOperation op2 = createOperation("createCollection", COLLECTION_NAME); + ops.add(op2); + + payload.setOperations(ops); + + List successfulOps = new ArrayList<>(); + Exception caughtException = null; + + for (MongoOperation op : payload.getOperations()) { + try { + op.getOperator(mongoDatabase).apply(null); + successfulOps.add(op); + } catch (Exception e) { + caughtException = e; + for (int i = successfulOps.size() - 1; i >= 0; i--) { + MongoOperation successfulOp = successfulOps.get(i); + if (successfulOp.getRollback() != null) { + successfulOp.getRollback().getOperator(mongoDatabase).apply(null); + } + } + break; + } + } + + assertNotNull(caughtException, "Should have caught an exception"); + assertTrue(collectionExists(COLLECTION_NAME), "Collection should still exist"); + + List docs = mongoDatabase.getCollection(COLLECTION_NAME) + .find() + .into(new ArrayList<>()); + assertEquals(1, docs.size(), "Should only have the original document"); + assertEquals("Existing", docs.get(0).getString("name"), "Only original document should remain"); + } + + @Test + @DisplayName("WHEN rollback is executed THEN operations are rolled back in reverse order") + void rollbackExecutesInReverseOrderTest() { + mongoDatabase.createCollection(COLLECTION_NAME); + List docs = new ArrayList<>(); + docs.add(new Document("name", "User1")); + docs.add(new Document("name", "User2")); + docs.add(new Document("name", "User3")); + mongoDatabase.getCollection(COLLECTION_NAME).insertMany(docs); + + MongoApplyPayload payload = new MongoApplyPayload(); + List ops = new ArrayList<>(); + + MongoOperation op1 = createInsertOperation(COLLECTION_NAME, "User1"); + MongoOperation rollback1 = createDeleteOperation(COLLECTION_NAME, "User1"); + op1.setRollback(rollback1); + ops.add(op1); + + MongoOperation op2 = createInsertOperation(COLLECTION_NAME, "User2"); + MongoOperation rollback2 = createDeleteOperation(COLLECTION_NAME, "User2"); + op2.setRollback(rollback2); + ops.add(op2); + + MongoOperation op3 = createInsertOperation(COLLECTION_NAME, "User3"); + MongoOperation rollback3 = createDeleteOperation(COLLECTION_NAME, "User3"); + op3.setRollback(rollback3); + ops.add(op3); + + payload.setOperations(ops); + + List operations = payload.getOperations(); + for (int i = operations.size() - 1; i >= 0; i--) { + MongoOperation op = operations.get(i); + if (op.getRollback() != null) { + op.getRollback().getOperator(mongoDatabase).apply(null); + } + } + + assertEquals(0, getDocumentCount(COLLECTION_NAME), "All documents should be deleted"); + } + + @Test + @DisplayName("WHEN operation has no rollback THEN it is skipped during rollback") + void missingRollbackIsSkippedTest() { + mongoDatabase.createCollection(COLLECTION_NAME); + + MongoApplyPayload payload = new MongoApplyPayload(); + List ops = new ArrayList<>(); + + MongoOperation op1 = createInsertOperation(COLLECTION_NAME, "User1"); + op1.setRollback(createDeleteOperation(COLLECTION_NAME, "User1")); + ops.add(op1); + + MongoOperation op2 = createInsertOperation(COLLECTION_NAME, "User2"); + ops.add(op2); + + MongoOperation op3 = createInsertOperation(COLLECTION_NAME, "User3"); + op3.setRollback(createDeleteOperation(COLLECTION_NAME, "User3")); + ops.add(op3); + + payload.setOperations(ops); + + for (MongoOperation op : payload.getOperations()) { + op.getOperator(mongoDatabase).apply(null); + } + + assertEquals(3, getDocumentCount(COLLECTION_NAME), "Should have 3 documents"); + + List operations = payload.getOperations(); + for (int i = operations.size() - 1; i >= 0; i--) { + MongoOperation op = operations.get(i); + if (op.getRollback() != null) { + op.getRollback().getOperator(mongoDatabase).apply(null); + } + } + + List docs = mongoDatabase.getCollection(COLLECTION_NAME) + .find() + .into(new ArrayList<>()); + assertEquals(1, docs.size(), "Only User2 should remain"); + assertEquals("User2", docs.get(0).getString("name")); + } + + @Test + @DisplayName("WHEN MongoOperation has rollback defined THEN getRollback returns it") + void getRollbackReturnsDefinedRollbackTest() { + MongoOperation op = createOperation("createCollection", COLLECTION_NAME); + MongoOperation rollback = createOperation("dropCollection", COLLECTION_NAME); + op.setRollback(rollback); + + assertEquals(rollback, op.getRollback()); + assertEquals("dropCollection", op.getRollback().getType()); + assertEquals(COLLECTION_NAME, op.getRollback().getCollection()); + } + + private MongoOperation createOperation(String type, String collection) { + MongoOperation op = new MongoOperation(); + op.setType(type); + op.setCollection(collection); + op.setParameters(new HashMap<>()); + return op; + } + + private MongoOperation createInsertOperation(String collection, String userName) { + MongoOperation op = new MongoOperation(); + op.setType("insert"); + op.setCollection(collection); + Map params = new HashMap<>(); + List> docs = new ArrayList<>(); + Map doc = new HashMap<>(); + doc.put("name", userName); + docs.add(doc); + params.put("documents", docs); + op.setParameters(params); + return op; + } + + private MongoOperation createDeleteOperation(String collection, String userName) { + MongoOperation op = new MongoOperation(); + op.setType("delete"); + op.setCollection(collection); + Map params = new HashMap<>(); + Map filter = new HashMap<>(); + filter.put("name", userName); + params.put("filter", filter); + op.setParameters(params); + return op; + } + + private boolean collectionExists(String collectionName) { + return mongoDatabase.listCollectionNames() + .into(new ArrayList<>()) + .contains(collectionName); + } + + private long getDocumentCount(String collectionName) { + return mongoDatabase.getCollection(collectionName).countDocuments(); + } +} diff --git a/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/RenameCollectionOperatorTest.java b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/RenameCollectionOperatorTest.java new file mode 100644 index 000000000..e9c576b54 --- /dev/null +++ b/templates/flamingock-mongodb-sync-template/src/test/java/io/flamingock/template/mongodb/operations/RenameCollectionOperatorTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2025 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.template.mongodb.operations; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.model.MongoOperation; +import io.flamingock.template.mongodb.model.operator.RenameCollectionOperator; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Testcontainers +class RenameCollectionOperatorTest { + + private static final String DB_NAME = "test"; + private static final String ORIGINAL_NAME = "originalCollection"; + private static final String RENAMED_NAME = "renamedCollection"; + + private static MongoClient mongoClient; + private static MongoDatabase mongoDatabase; + + @Container + public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6")); + + @BeforeAll + static void beforeAll() { + mongoClient = MongoClients.create(MongoClientSettings + .builder() + .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString())) + .build()); + mongoDatabase = mongoClient.getDatabase(DB_NAME); + } + + @BeforeEach + void setupEach() { + mongoDatabase.getCollection(ORIGINAL_NAME).drop(); + mongoDatabase.getCollection(RENAMED_NAME).drop(); + } + + @Test + @DisplayName("WHEN renameCollection operator is applied THEN collection is renamed") + void renameCollectionTest() { + mongoDatabase.createCollection(ORIGINAL_NAME); + assertTrue(collectionExists(ORIGINAL_NAME), "Original collection should exist before rename"); + + MongoOperation operation = new MongoOperation(); + operation.setType("renameCollection"); + operation.setCollection(ORIGINAL_NAME); + Map params = new HashMap<>(); + params.put("target", RENAMED_NAME); + operation.setParameters(params); + + RenameCollectionOperator operator = new RenameCollectionOperator(mongoDatabase, operation); + operator.apply(null); + + assertFalse(collectionExists(ORIGINAL_NAME), "Original collection should not exist after rename"); + assertTrue(collectionExists(RENAMED_NAME), "Renamed collection should exist"); + } + + private boolean collectionExists(String collectionName) { + List collections = mongoDatabase.listCollectionNames().into(new ArrayList<>()); + return collections.contains(collectionName); + } +}