diff --git a/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java b/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java index cda3f84..138c052 100644 --- a/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java +++ b/src/main/java/io/flamingock/template/mongodb/MongoChangeTemplate.java @@ -23,6 +23,7 @@ import io.flamingock.api.annotations.Rollback; import io.flamingock.api.template.AbstractChangeTemplate; import io.flamingock.api.template.wrappers.TemplateVoid; +import io.flamingock.api.template.TemplateValidationContext; import io.flamingock.template.mongodb.model.MongoOperation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -86,7 +87,7 @@ public MongoChangeTemplate() { * Executes the apply operation for the current step. *

* Precondition: {@code applyPayload} has been structurally validated at load time - * via {@link MongoOperation#validate()}, which delegates to the appropriate + * via {@link MongoOperation#validate(TemplateValidationContext)}, which delegates to the appropriate * {@code ParametersValidator} for each operation type. No validation is performed here. */ @Apply @@ -99,7 +100,7 @@ public void apply(MongoDatabase db, @Nullable ClientSession clientSession) { * Executes the rollback operation for the current step. *

* Precondition: {@code rollbackPayload} has been structurally validated at load time - * via {@link MongoOperation#validate()}, which delegates to the appropriate + * via {@link MongoOperation#validate(TemplateValidationContext)}, which delegates to the appropriate * {@code ParametersValidator} for each operation type. No validation is performed here. */ @Rollback diff --git a/src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java b/src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java index 65d5d76..b504141 100644 --- a/src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java +++ b/src/main/java/io/flamingock/template/mongodb/model/MongoOperation.java @@ -20,6 +20,7 @@ import io.flamingock.api.NonLockGuardedType; import io.flamingock.api.template.TemplatePayload; import io.flamingock.api.template.TemplatePayloadValidationError; +import io.flamingock.api.template.TemplateValidationContext; import io.flamingock.template.mongodb.model.operator.MongoOperator; import io.flamingock.template.mongodb.validation.CollectionValidator; import io.flamingock.template.mongodb.validation.TypeValidator; @@ -31,6 +32,7 @@ @NonLockGuarded(NonLockGuardedType.NONE) public class MongoOperation implements TemplatePayload { + private String type; private String collection; private Map parameters; @@ -87,7 +89,7 @@ public String toString() { private static final CollectionValidator COLLECTION_VALIDATOR = new CollectionValidator(); @Override - public List validate() { + public List validate(TemplateValidationContext context) { List typeErrors = TYPE_VALIDATOR.validate(this); List errors = new ArrayList<>(typeErrors); @@ -101,6 +103,13 @@ public List validate() { errors.addAll(COLLECTION_VALIDATOR.validate(this)); errors.addAll(operationType.getOperationValidator().validate(this)); + if (!operationType.isTransactional() && context.isTransactional()) { + errors.add(new TemplatePayloadValidationError("type", + "Operation type '" + type + "' does not support transactions. " + + "Transactional changes require all operations to be transactional " + + "(insert, update, delete).")); + } + return errors; } } \ No newline at end of file diff --git a/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java b/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java index 9e006e6..798fee9 100644 --- a/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java +++ b/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java @@ -45,28 +45,31 @@ public enum MongoOperationType { - CREATE_COLLECTION("createCollection", CreateCollectionOperator::new, new NoParametersValidator("CreateCollection")), - CREATE_INDEX("createIndex", CreateIndexOperator::new, new CreateIndexParametersValidator()), - INSERT("insert", InsertOperator::new, new InsertParametersValidator()), - UPDATE("update", UpdateOperator::new, new UpdateParametersValidator()), - DELETE("delete", DeleteOperator::new, new DeleteParametersValidator()), - DROP_COLLECTION("dropCollection", DropCollectionOperator::new, new NoParametersValidator("DropCollection")), - DROP_INDEX("dropIndex", DropIndexOperator::new, new DropIndexParametersValidator()), - RENAME_COLLECTION("renameCollection", RenameCollectionOperator::new, new RenameCollectionParametersValidator()), - MODIFY_COLLECTION("modifyCollection", ModifyCollectionOperator::new, new ModifyCollectionParametersValidator()), - CREATE_VIEW("createView", CreateViewOperator::new, new CreateViewParametersValidator()), - DROP_VIEW("dropView", DropViewOperator::new, new NoParametersValidator("DropView")); + CREATE_COLLECTION("createCollection", CreateCollectionOperator::new, new NoParametersValidator("CreateCollection"), false), + CREATE_INDEX("createIndex", CreateIndexOperator::new, new CreateIndexParametersValidator(), false), + INSERT("insert", InsertOperator::new, new InsertParametersValidator(), true), + UPDATE("update", UpdateOperator::new, new UpdateParametersValidator(), true), + DELETE("delete", DeleteOperator::new, new DeleteParametersValidator(), true), + DROP_COLLECTION("dropCollection", DropCollectionOperator::new, new NoParametersValidator("DropCollection"), false), + DROP_INDEX("dropIndex", DropIndexOperator::new, new DropIndexParametersValidator(), false), + RENAME_COLLECTION("renameCollection", RenameCollectionOperator::new, new RenameCollectionParametersValidator(), false), + MODIFY_COLLECTION("modifyCollection", ModifyCollectionOperator::new, new ModifyCollectionParametersValidator(), false), + CREATE_VIEW("createView", CreateViewOperator::new, new CreateViewParametersValidator(), false), + DROP_VIEW("dropView", DropViewOperator::new, new NoParametersValidator("DropView"), false); private final String value; private final BiFunction createOperatorFunction; private final OperationValidator operationValidator; + private final boolean transactional; MongoOperationType(String value, BiFunction createOperatorFunction, - OperationValidator operationValidator) { + OperationValidator operationValidator, + boolean transactional) { this.value = value; this.createOperatorFunction = createOperatorFunction; this.operationValidator = operationValidator; + this.transactional = transactional; } public static MongoOperationType findByTypeOrThrow(String typeValue) { @@ -90,6 +93,10 @@ public OperationValidator getOperationValidator() { return operationValidator; } + public boolean isTransactional() { + return transactional; + } + private boolean matches(String operationType) { return this.value.equals(operationType); } diff --git a/src/test/java/io/flamingock/template/mongodb/model/MongoOperationValidateTest.java b/src/test/java/io/flamingock/template/mongodb/model/MongoOperationValidateTest.java index a0f3cc4..956f50b 100644 --- a/src/test/java/io/flamingock/template/mongodb/model/MongoOperationValidateTest.java +++ b/src/test/java/io/flamingock/template/mongodb/model/MongoOperationValidateTest.java @@ -16,9 +16,12 @@ package io.flamingock.template.mongodb.model; import io.flamingock.api.template.TemplatePayloadValidationError; +import io.flamingock.api.template.TemplateValidationContext; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.util.*; @@ -27,6 +30,14 @@ class MongoOperationValidateTest { + private static final TemplateValidationContext NON_TRANSACTIONAL_CONTEXT = new TemplateValidationContext(); + + private static TemplateValidationContext transactionalContext() { + TemplateValidationContext ctx = new TemplateValidationContext(); + ctx.setTransactional(true); + return ctx; + } + @Nested @DisplayName("Common Validation Tests") class CommonValidationTests { @@ -38,7 +49,7 @@ void nullOperationTypeTest() { op.setType(null); op.setCollection("test"); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("type", errors.get(0).getField()); @@ -52,7 +63,7 @@ void emptyOperationTypeTest() { op.setType(""); op.setCollection("test"); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("type", errors.get(0).getField()); @@ -66,7 +77,7 @@ void unknownOperationTypeTest() { op.setType("unknownType"); op.setCollection("test"); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("type", errors.get(0).getField()); @@ -80,7 +91,7 @@ void nullCollectionTest() { op.setType("createCollection"); op.setCollection(null); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("collection", errors.get(0).getField()); @@ -94,7 +105,7 @@ void emptyCollectionTest() { op.setType("createCollection"); op.setCollection(""); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("collection", errors.get(0).getField()); @@ -108,7 +119,7 @@ void collectionWithDollarSignTest() { op.setType("createCollection"); op.setCollection("test$collection"); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("collection", errors.get(0).getField()); @@ -122,7 +133,7 @@ void collectionWithNullCharTest() { op.setType("createCollection"); op.setCollection("test\0collection"); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("collection", errors.get(0).getField()); @@ -142,7 +153,7 @@ void insertMissingParametersTest() { op.setCollection("test"); op.setParameters(null); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters", errors.get(0).getField()); @@ -157,7 +168,7 @@ void insertMissingDocumentsTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.documents", errors.get(0).getField()); @@ -174,7 +185,7 @@ void insertEmptyDocumentsTest() { params.put("documents", new ArrayList<>()); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.documents", errors.get(0).getField()); @@ -193,7 +204,7 @@ void insertNullDocumentElementTest() { params.put("documents", docs); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertTrue(errors.get(0).getField().contains("parameters.documents")); @@ -210,7 +221,7 @@ void insertDocumentNotMapTest() { params.put("documents", Arrays.asList("hello")); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.documents[0]", errors.get(0).getField()); @@ -229,7 +240,7 @@ void insertDocumentMixedTypesTest() { params.put("documents", Arrays.asList(validDoc, 123)); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.documents[1]", errors.get(0).getField()); @@ -246,7 +257,7 @@ void insertDocumentsWrongTypeTest() { params.put("documents", "not a list"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.documents", errors.get(0).getField()); @@ -268,7 +279,7 @@ void insertOptionsWrongTypeTest() { params.put("options", "not a document"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.options", errors.get(0).getField()); @@ -289,7 +300,7 @@ void insertValidTest() { params.put("documents", docs); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -307,7 +318,7 @@ void updateMissingParametersTest() { op.setCollection("test"); op.setParameters(null); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters", errors.get(0).getField()); @@ -326,7 +337,7 @@ void updateMissingFilterTest() { params.put("update", update); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.filter", errors.get(0).getField()); @@ -343,7 +354,7 @@ void updateMissingUpdateParamTest() { params.put("filter", new HashMap<>()); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.update", errors.get(0).getField()); @@ -361,7 +372,7 @@ void updateParamWrongTypeTest() { params.put("update", "not a document"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.update", errors.get(0).getField()); @@ -381,7 +392,7 @@ void updateFilterWrongTypeTest() { params.put("update", update); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.filter", errors.get(0).getField()); @@ -402,7 +413,7 @@ void updateOptionsWrongTypeTest() { params.put("options", "not a document"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.options", errors.get(0).getField()); @@ -423,7 +434,7 @@ void updateMultiWrongTypeTest() { params.put("multi", "yes"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.multi", errors.get(0).getField()); @@ -438,7 +449,7 @@ void updateMissingBothTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(2, errors.size()); } @@ -458,7 +469,7 @@ void updateValidTest() { params.put("update", update); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -476,7 +487,7 @@ void deleteMissingFilterTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.filter", errors.get(0).getField()); @@ -493,7 +504,7 @@ void deleteFilterWrongTypeTest() { params.put("filter", "not a document"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.filter", errors.get(0).getField()); @@ -510,7 +521,7 @@ void deleteEmptyFilterTest() { params.put("filter", new HashMap<>()); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -528,7 +539,7 @@ void deleteMultiWrongTypeTest() { params.put("multi", "true"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.multi", errors.get(0).getField()); @@ -547,7 +558,7 @@ void deleteValidTest() { params.put("filter", filter); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -565,7 +576,7 @@ void createIndexMissingParametersTest() { op.setCollection("test"); op.setParameters(null); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters", errors.get(0).getField()); @@ -580,7 +591,7 @@ void createIndexMissingKeysTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.keys", errors.get(0).getField()); @@ -597,7 +608,7 @@ void createIndexEmptyKeysTest() { params.put("keys", new HashMap<>()); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.keys", errors.get(0).getField()); @@ -614,7 +625,7 @@ void createIndexKeysWrongTypeTest() { params.put("keys", "not a map"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.keys", errors.get(0).getField()); @@ -634,7 +645,7 @@ void createIndexOptionsWrongTypeTest() { params.put("options", "not a document"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.options", errors.get(0).getField()); @@ -653,7 +664,7 @@ void createIndexValidTest() { params.put("keys", keys); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -671,7 +682,7 @@ void dropIndexMissingBothTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertTrue(errors.get(0).getMessage().contains("'indexName' or 'keys'")); @@ -687,7 +698,7 @@ void dropIndexKeysWrongTypeTest() { params.put("keys", "not a map"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.keys", errors.get(0).getField()); @@ -704,7 +715,7 @@ void dropIndexIndexNameWrongTypeTest() { params.put("indexName", 123); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.indexName", errors.get(0).getField()); @@ -724,7 +735,7 @@ void dropIndexBothIndexNameAndKeysTest() { params.put("keys", keys); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters", errors.get(0).getField()); @@ -741,7 +752,7 @@ void dropIndexWithIndexNameTest() { params.put("indexName", "email_index"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -758,7 +769,7 @@ void dropIndexWithKeysTest() { params.put("keys", keys); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -776,7 +787,7 @@ void renameCollectionMissingTargetTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.target", errors.get(0).getField()); @@ -793,7 +804,7 @@ void renameCollectionEmptyTargetTest() { params.put("target", ""); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.target", errors.get(0).getField()); @@ -810,7 +821,7 @@ void renameCollectionTargetWithDollarSignTest() { params.put("target", "new$Name"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.target", errors.get(0).getField()); @@ -827,7 +838,7 @@ void renameCollectionTargetWithNullCharTest() { params.put("target", "new\0Name"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.target", errors.get(0).getField()); @@ -845,7 +856,7 @@ void renameCollectionOptionsWrongTypeTest() { params.put("options", "not a document"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.options", errors.get(0).getField()); @@ -862,7 +873,7 @@ void renameCollectionValidTest() { params.put("target", "newName"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -880,7 +891,7 @@ void createViewMissingParametersTest() { op.setCollection("testView"); op.setParameters(null); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters", errors.get(0).getField()); @@ -897,7 +908,7 @@ void createViewMissingViewOnTest() { params.put("pipeline", Collections.singletonList(new HashMap<>())); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.viewOn", errors.get(0).getField()); @@ -914,7 +925,7 @@ void createViewMissingPipelineTest() { params.put("viewOn", "sourceCollection"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.pipeline", errors.get(0).getField()); @@ -932,7 +943,7 @@ void createViewPipelineWrongTypeTest() { params.put("pipeline", "not a list"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.pipeline", errors.get(0).getField()); @@ -950,7 +961,7 @@ void createViewPipelineStageNotMapTest() { params.put("pipeline", Arrays.asList("invalid")); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.pipeline[0]", errors.get(0).getField()); @@ -968,7 +979,7 @@ void createViewViewOnContainsDollarTest() { params.put("pipeline", Collections.singletonList(new HashMap<>())); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.viewOn", errors.get(0).getField()); @@ -986,7 +997,7 @@ void createViewViewOnContainsNullCharTest() { params.put("pipeline", Collections.singletonList(new HashMap<>())); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.viewOn", errors.get(0).getField()); @@ -1004,7 +1015,7 @@ void createViewViewOnWrongTypeTest() { params.put("pipeline", Collections.singletonList(new HashMap<>())); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.viewOn", errors.get(0).getField()); @@ -1023,7 +1034,7 @@ void createViewOptionsWrongTypeTest() { params.put("options", "not a document"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.options", errors.get(0).getField()); @@ -1041,7 +1052,7 @@ void createViewValidTest() { params.put("pipeline", Collections.singletonList(new HashMap<>())); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -1058,7 +1069,7 @@ void createCollectionValidTest() { op.setType("createCollection"); op.setCollection("test"); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -1070,7 +1081,7 @@ void dropCollectionValidTest() { op.setType("dropCollection"); op.setCollection("test"); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -1082,7 +1093,7 @@ void dropViewValidTest() { op.setType("dropView"); op.setCollection("testView"); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -1097,7 +1108,7 @@ void createCollectionWithParametersTest() { params.put("capped", true); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters", errors.get(0).getField()); @@ -1112,7 +1123,7 @@ void createCollectionEmptyParametersTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -1127,7 +1138,7 @@ void dropCollectionWithParametersTest() { params.put("writeConcern", "majority"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters", errors.get(0).getField()); @@ -1142,7 +1153,7 @@ void dropCollectionEmptyParametersTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -1157,7 +1168,7 @@ void dropViewWithParametersTest() { params.put("force", true); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters", errors.get(0).getField()); @@ -1172,7 +1183,7 @@ void dropViewEmptyParametersTest() { op.setCollection("testView"); op.setParameters(new HashMap<>()); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -1190,7 +1201,7 @@ void modifyCollectionNullParametersTest() { op.setCollection("test"); op.setParameters(null); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters", errors.get(0).getField()); @@ -1205,7 +1216,7 @@ void modifyCollectionEmptyParametersTest() { op.setCollection("test"); op.setParameters(new HashMap<>()); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters", errors.get(0).getField()); @@ -1222,7 +1233,7 @@ void modifyCollectionInvalidValidationLevelTest() { params.put("validationLevel", "banana"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.validationLevel", errors.get(0).getField()); @@ -1239,7 +1250,7 @@ void modifyCollectionInvalidValidationActionTest() { params.put("validationAction", "ignore"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.validationAction", errors.get(0).getField()); @@ -1256,7 +1267,7 @@ void modifyCollectionValidatorWrongTypeTest() { params.put("validator", "not a map"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.validator", errors.get(0).getField()); @@ -1274,7 +1285,7 @@ void modifyCollectionMultipleInvalidParamsTest() { params.put("validationAction", "ignore"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(2, errors.size()); } @@ -1291,7 +1302,7 @@ void modifyCollectionValidValidatorOnlyTest() { params.put("validator", validator); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -1307,7 +1318,7 @@ void modifyCollectionValidValidationLevelOnlyTest() { params.put("validationLevel", level); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty(), "Expected no errors for validationLevel='" + level + "'"); } @@ -1324,7 +1335,7 @@ void modifyCollectionValidValidationActionOnlyTest() { params.put("validationAction", action); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty(), "Expected no errors for validationAction='" + action + "'"); } @@ -1344,7 +1355,7 @@ void modifyCollectionAllValidParamsTest() { params.put("validationAction", "error"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -1364,7 +1375,7 @@ void multipleErrorsTest() { params.put("documents", new ArrayList<>()); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(2, errors.size()); } @@ -1389,7 +1400,7 @@ void insertUnrecognizedKeyTest() { params.put("unknownKey", "value"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.unknownKey", errors.get(0).getField()); @@ -1410,7 +1421,7 @@ void updateUnrecognizedKeyTest() { params.put("typoKey", "value"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.typoKey", errors.get(0).getField()); @@ -1428,7 +1439,7 @@ void deleteUnrecognizedKeyTest() { params.put("unknownKey", "value"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.unknownKey", errors.get(0).getField()); @@ -1448,7 +1459,7 @@ void createIndexUnrecognizedKeyTest() { params.put("badParam", "value"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.badParam", errors.get(0).getField()); @@ -1466,7 +1477,7 @@ void dropIndexUnrecognizedKeyTest() { params.put("extra", "value"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.extra", errors.get(0).getField()); @@ -1484,7 +1495,7 @@ void renameCollectionUnrecognizedKeyTest() { params.put("badParam", "value"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.badParam", errors.get(0).getField()); @@ -1503,7 +1514,7 @@ void createViewUnrecognizedKeyTest() { params.put("badParam", "value"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.badParam", errors.get(0).getField()); @@ -1521,7 +1532,7 @@ void modifyCollectionUnrecognizedKeyTest() { params.put("unknownKey", "value"); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.unknownKey", errors.get(0).getField()); @@ -1550,7 +1561,7 @@ void insertUnrecognizedOptionKeyTest() { params.put("options", options); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.options.banana", errors.get(0).getField()); @@ -1573,7 +1584,7 @@ void updateUnrecognizedOptionKeyTest() { params.put("options", options); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.options.unknownOption", errors.get(0).getField()); @@ -1595,7 +1606,7 @@ void createIndexUnrecognizedOptionKeyTest() { params.put("options", options); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.options.invalidOption", errors.get(0).getField()); @@ -1616,7 +1627,7 @@ void createViewUnrecognizedOptionKeyTest() { params.put("options", options); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.options.unknownViewOption", errors.get(0).getField()); @@ -1636,7 +1647,7 @@ void renameCollectionUnrecognizedOptionKeyTest() { params.put("options", options); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(1, errors.size()); assertEquals("parameters.options.badOption", errors.get(0).getField()); @@ -1659,7 +1670,7 @@ void createIndexRecognizedOptionKeyTest() { params.put("options", options); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertTrue(errors.isEmpty()); } @@ -1682,9 +1693,122 @@ void insertMultipleUnrecognizedOptionKeysTest() { params.put("options", options); op.setParameters(params); - List errors = op.validate(); + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); assertEquals(2, errors.size()); } } + + @Nested + @DisplayName("Transactional Validation Tests") + class TransactionalValidationTests { + + private final TemplateValidationContext TRANSACTIONAL_CONTEXT = transactionalContext(); + + @ParameterizedTest(name = "WHEN transactional context + non-transactional op ''{0}'' THEN error") + @ValueSource(strings = { + "createCollection", "dropCollection", "createIndex", "dropIndex", + "renameCollection", "modifyCollection", "createView", "dropView" + }) + void transactionalContextWithNonTransactionalOpReturnsError(String opType) { + MongoOperation op = buildValidOperation(opType); + + List errors = op.validate(TRANSACTIONAL_CONTEXT); + + assertTrue(errors.stream().anyMatch(e -> + "type".equals(e.getField()) && e.getMessage().contains("does not support transactions")), + "Expected transactional incompatibility error for type '" + opType + "'"); + } + + @ParameterizedTest(name = "WHEN transactional context + transactional op ''{0}'' THEN no transactional error") + @ValueSource(strings = {"insert", "update", "delete"}) + void transactionalContextWithTransactionalOpReturnsNoError(String opType) { + MongoOperation op = buildValidOperation(opType); + + List errors = op.validate(TRANSACTIONAL_CONTEXT); + + assertTrue(errors.stream().noneMatch(e -> + "type".equals(e.getField()) && e.getMessage().contains("does not support transactions")), + "Expected no transactional incompatibility error for type '" + opType + "'"); + } + + @ParameterizedTest(name = "WHEN non-transactional context + transactional op ''{0}'' THEN no error") + @ValueSource(strings = {"insert", "update", "delete"}) + void nonTransactionalContextWithTransactionalOpReturnsNoError(String opType) { + MongoOperation op = buildValidOperation(opType); + + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); + + assertTrue(errors.isEmpty(), + "Expected no errors for transactional op '" + opType + "' in non-transactional context"); + } + + @ParameterizedTest(name = "WHEN non-transactional context + non-transactional op ''{0}'' THEN no error") + @ValueSource(strings = { + "createCollection", "dropCollection", "createIndex", "dropIndex", + "renameCollection", "modifyCollection", "createView", "dropView" + }) + void nonTransactionalContextWithNonTransactionalOpReturnsNoError(String opType) { + MongoOperation op = buildValidOperation(opType); + + List errors = op.validate(NON_TRANSACTIONAL_CONTEXT); + + assertTrue(errors.isEmpty(), + "Expected no errors for non-transactional op '" + opType + "' in non-transactional context"); + } + + private MongoOperation buildValidOperation(String opType) { + MongoOperation op = new MongoOperation(); + op.setType(opType); + op.setCollection("testCollection"); + op.setParameters(buildRequiredParams(opType)); + return op; + } + + private Map buildRequiredParams(String opType) { + Map params = new HashMap<>(); + switch (opType) { + case "insert": + List> docs = new ArrayList<>(); + Map doc = new HashMap<>(); + doc.put("name", "test"); + docs.add(doc); + params.put("documents", docs); + break; + case "update": + params.put("filter", new HashMap<>()); + Map update = new HashMap<>(); + update.put("$set", Collections.singletonMap("field", "value")); + params.put("update", update); + break; + case "delete": + params.put("filter", new HashMap<>()); + break; + case "createIndex": + params.put("keys", Collections.singletonMap("field", 1)); + break; + case "dropIndex": + params.put("indexName", "test_index"); + break; + case "renameCollection": + params.put("target", "newName"); + break; + case "modifyCollection": + params.put("validationLevel", "strict"); + break; + case "createView": + params.put("viewOn", "sourceCollection"); + params.put("pipeline", Collections.singletonList(new HashMap<>())); + break; + case "createCollection": + case "dropCollection": + case "dropView": + // No parameters needed + return null; + default: + break; + } + return params; + } + } }