diff --git a/ANALYSIS.md b/ANALYSIS.md
index 3228c8f..a39899f 100644
--- a/ANALYSIS.md
+++ b/ANALYSIS.md
@@ -4,7 +4,7 @@
**Flamingock Core:** v1.2.0
**Java Target:** 8
**MongoDB Driver:** 4.0.0 (compileOnly)
-**Last Updated:** 2026-02-28
+**Last Updated:** 2026-03-02
---
@@ -83,20 +83,22 @@ All 10 issues identified in the original analysis have been resolved through mul
|------------------|---------------------|----------------------------|:-------------:|:---------------------------------------------------:|:-------------------------------:|:-------------------------:|:----------------------------------:|:------------------------------:|
| createCollection | `CREATE_COLLECTION` | `CreateCollectionOperator` | No | NoParametersValidator | None | Ignores (base class logs) | `CreateCollectionOperatorTest` (4) | YAML `_0001` |
| dropCollection | `DROP_COLLECTION` | `DropCollectionOperator` | No | NoParametersValidator | None | Ignores (base class logs) | `DropCollectionOperatorTest` (2) | YAML `_0004` rollback |
-| insert | `INSERT` | `InsertOperator` | Yes | Full (documents, options) | `InsertOptionsMapper` | Full | `InsertOperatorTest` (4) | YAML `_0002`, `_0003`, `_0005` |
-| update | `UPDATE` | `UpdateOperator` | Yes | Full (filter, update, options) | `UpdateOptionsMapper` | Full | `UpdateOperatorTest` (7) | None |
-| delete | `DELETE` | `DeleteOperator` | Yes | Full (filter, multi) | None | Full | `DeleteOperatorTest` (6) | YAML `_0002` rollback |
-| createIndex | `CREATE_INDEX` | `CreateIndexOperator` | No | Full (keys, options) | `IndexOptionsMapper` | Ignores (base class logs) | `CreateIndexOperatorTest` (3) | YAML `_0003`, `_0005` |
+| insert | `INSERT` | `InsertOperator` | Yes | Full (documents, options) | `InsertOptionsMapper` | Full | `InsertOperatorTest` (3) + Txn (5) + Options (3) | YAML `_0002`, `_0003`, `_0005` |
+| update | `UPDATE` | `UpdateOperator` | Yes | Full (filter, update, options) | `UpdateOptionsMapper` | Full | `UpdateOperatorTest` (7) + Txn (3) | None |
+| delete | `DELETE` | `DeleteOperator` | Yes | Full (filter, multi) | None | Full | `DeleteOperatorTest` (5) + Txn (3) | YAML `_0002` rollback |
+| createIndex | `CREATE_INDEX` | `CreateIndexOperator` | No | Full (keys, options) | `IndexOptionsMapper` | Ignores (base class logs) | `CreateIndexOperatorTest` (4) + Options (3) | YAML `_0003`, `_0005` |
| dropIndex | `DROP_INDEX` | `DropIndexOperator` | No | indexName or keys | None | Ignores (base class logs) | `DropIndexOperatorTest` (7) | YAML `_0005` rollback |
| renameCollection | `RENAME_COLLECTION` | `RenameCollectionOperator` | No | Full (target, options) | `RenameCollectionOptionsMapper` | Ignores (base class logs) | `RenameCollectionOperatorTest` (6) | None |
-| modifyCollection | `MODIFY_COLLECTION` | `ModifyCollectionOperator` | No | Full (validator, validationLevel, validationAction) | None | Ignores (base class logs) | `ModifyCollectionOperatorTest` (2) | None |
+| modifyCollection | `MODIFY_COLLECTION` | `ModifyCollectionOperator` | No | Full (validator, validationLevel, validationAction) | None | Ignores (base class logs) | `ModifyCollectionOperatorTest` (5) | None |
| createView | `CREATE_VIEW` | `CreateViewOperator` | No | Full (viewOn, pipeline, options) | `CreateViewOptionsMapper` | Ignores (base class logs) | `CreateViewOperatorTest` (4) | None |
| dropView | `DROP_VIEW` | `DropViewOperator` | No | NoParametersValidator | None | Ignores (base class logs) | `DropViewOperatorTest` (2) | None |
### Coverage Notes:
- **5 of 11 operations** have integration test coverage via YAML changes (createCollection, insert, delete, createIndex, dropIndex)
- **6 operations** are only tested at the operator unit level: update, renameCollection, modifyCollection, createView, dropView, dropCollection (though dropCollection appears in YAML rollback steps)
-- **0 operations** are tested with `ClientSession` (transactional path) — framework integration tests cover this implicitly
+- **3 transactional operators** (insert, update, delete) now have dedicated `ClientSession` tests — 11 tests across 3 transactional test classes exercise session-with-commit, session-with-options, and session-with-abort paths
+- **Options integration** tested end-to-end: insert (`bypassDocumentValidation`, `ordered`), createIndex (`expireAfterSeconds`, `sparse`, `partialFilterExpression`), update (`collation`)
+- `MongoChangeTemplate.validateSession()` tested for both apply and rollback paths
- All 11 operations now have comprehensive load-time validation via `MongoOperation.validate()`, including unrecognized option key detection for the 5 operations that support `options`
- All 8 DDL operators now consistently delegate session handling to the base class `MongoOperator.logOperation()`
- All operators wrap MongoDB driver exceptions with template-level context via `MongoTemplateExecutionException`
@@ -109,42 +111,51 @@ All 10 issues identified in the original analysis have been resolved through mul
| Test Class | Test Count | Type | Docker Required |
|-------------------------------------|:----------:|:--------------------------------------:|:---------------:|
-| `MongoChangeTemplateTest` | 6 | Integration (full Flamingock pipeline) | Yes |
+| `MongoChangeTemplateTest` | 7 | Integration (full Flamingock pipeline) | Yes |
| `MongoOperationValidateTest` | 94 | Unit (pure logic, nested classes) | No |
-| `InsertOperatorTest` | 4 | Integration (operator-level) | Yes |
+| `InsertOperatorTest` | 3 | Integration (operator-level) | Yes |
+| `InsertOperatorTransactionalTest` | 5 | Integration (transactional path) | Yes |
+| `InsertOperatorOptionsTest` | 3 | Integration (options end-to-end) | Yes |
| `UpdateOperatorTest` | 7 | Integration (operator-level) | Yes |
-| `DeleteOperatorTest` | 6 | Integration (operator-level) | Yes |
-| `CreateIndexOperatorTest` | 3 | Integration (operator-level) | Yes |
-| `DropIndexOperatorTest` | 7 | Integration (operator-level) | Yes |
-| `CreateCollectionOperatorTest` | 4 | Integration (operator-level) | Yes |
+| `UpdateOperatorTransactionalTest` | 3 | Integration (transactional path) | Yes |
+| `DeleteOperatorTest` | 5 | Integration (operator-level) | Yes |
+| `DeleteOperatorTransactionalTest` | 3 | Integration (transactional path) | Yes |
+| `CreateIndexOperatorTest` | 4 | Integration (operator-level) | Yes |
+| `CreateIndexOperatorOptionsTest` | 3 | Integration (options end-to-end) | Yes |
+| `DropIndexOperatorTest` | 6 | Integration (operator-level) | Yes |
+| `CreateCollectionOperatorTest` | 3 | Integration (operator-level) | Yes |
| `DropCollectionOperatorTest` | 2 | Integration (operator-level) | Yes |
-| `RenameCollectionOperatorTest` | 6 | Integration (operator-level) | Yes |
-| `ModifyCollectionOperatorTest` | 2 | Integration (operator-level) | Yes |
-| `CreateViewOperatorTest` | 4 | Integration (operator-level) | Yes |
+| `RenameCollectionOperatorTest` | 5 | Integration (operator-level) | Yes |
+| `ModifyCollectionOperatorTest` | 5 | Integration (operator-level) | Yes |
+| `CreateViewOperatorTest` | 3 | Integration (operator-level) | Yes |
| `DropViewOperatorTest` | 2 | Integration (operator-level) | Yes |
-| `MultipleOperationsTest` | 4 | Integration (operator-level) | Yes |
+| `MultipleOperationsTest` | 3 | Integration (operator-level) | Yes |
| `IndexOptionsMapperTest` | 22 | Unit | No |
-| `MapperUtilTest` | 34 | Unit | No |
+| `MapperUtilTest` | 17 | Unit | No |
+| `BsonConverterTest` | 28 | Unit | No |
| `InsertOptionsMapperTest` | 11 | Unit | No |
| `UpdateOptionsMapperTest` | 8 | Unit | No |
| `RenameCollectionOptionsMapperTest` | 4 | Unit | No |
| `CreateViewOptionsMapperTest` | 3 | Unit | No |
| `MongoOperatorExceptionWrappingTest`| 5 | Unit | No |
-| **Total** | **~237** | | |
+| **Total** | **~265** | | |
-Unit tests (no Docker): ~180 | Integration tests (Docker required): ~57
+Unit tests (no Docker): ~192 | Integration tests (Docker required): ~73
-### 4.2 Remaining Test Gaps
+### 4.2 Remaining Test Gaps — ALL RESOLVED
-**P0 — Transactional path:**
-No dedicated tests exercise operators with a `ClientSession`. The transactional execution path is only tested implicitly through the `MongoChangeTemplateTest` integration pipeline. No test verifies transaction commit after successful apply, transaction rollback after failed apply, or behavior when `isTransactional=true` but `clientSession=null`. This is acceptable for now since the framework manages session lifecycle, but explicit tests would increase confidence.
+**P0 — ~~Transactional path~~** RESOLVED:
+11 dedicated transactional tests across 3 test classes (`InsertOperatorTransactionalTest`, `UpdateOperatorTransactionalTest`, `DeleteOperatorTransactionalTest`) now exercise all `clientSession != null` branches. Tests cover session-with-commit, session-with-options, and session-with-abort (proving the session is actually passed through to the driver). `AbstractTransactionalOperatorTest` provides shared session lifecycle. Additionally, 2 tests in `MongoChangeTemplateTest` verify that `validateSession()` throws `IllegalArgumentException` when `isTransactional=true` and `clientSession=null` for both apply and rollback paths.
-**P1 — Options integration:**
-Mapper unit tests are comprehensive (82 tests), but no operator test verifies behavior with non-default options end-to-end (e.g., `insert` with `bypassDocumentValidation`, `createIndex` with `unique`, `update` with `upsert`). The mapper tests validate conversion correctness; the gap is confirming that converted options produce the expected MongoDB behavior.
+**P1 — ~~Options integration~~** RESOLVED:
+9 end-to-end options tests now verify that YAML-mapped options produce the expected MongoDB behavior: `InsertOperatorOptionsTest` (3 tests: `bypassDocumentValidation`, `ordered: false`, `ordered: true`), `CreateIndexOperatorOptionsTest` (3 tests: TTL `expireAfterSeconds`, `sparse`, `partialFilterExpression`), and `UpdateOperatorTest` (+1 test: `collation` with case-insensitive matching via `locale: en, strength: 2`).
**P1 — ~~Idempotency~~** RESOLVED:
12 idempotency tests now cover all 4 DDL operators that needed handling: `CreateCollectionOperatorTest` (+2), `DropIndexOperatorTest` (+4), `CreateViewOperatorTest` (+2), `RenameCollectionOperatorTest` (+4). Tests verify both the idempotent skip path and the error path for genuine conflicts.
+**P2 — ~~DDL operator gaps~~** RESOLVED:
+8 additional tests fill DDL gaps: `DropCollectionOperatorTest` (+1: native idempotency of drop on non-existent collection), `DropViewOperatorTest` (+1: same for views), `CreateIndexOperatorTest` (+2: identical index idempotency, conflicting index name with different keys), `ModifyCollectionOperatorTest` (+4: validation enforcement, metadata assertions for `validationLevel`/`validationAction`, partial parameters, idempotent re-application).
+
### 4.2.1 Previously Identified Gaps — Now Resolved
| Original Gap | Status | How Resolved |
@@ -156,6 +167,10 @@ Mapper unit tests are comprehensive (82 tests), but no operator test verifies be
| P1: Edge case getters | RESOLVED | Type validation at load time prevents `ClassCastException` in getters |
| P1: deleteOne/deleteMany | RESOLVED | `multi` parameter added and tested (6 tests in `DeleteOperatorTest`) |
| P1: Collation YAML | RESOLVED | Map-to-Collation conversion added with tests in `MapperUtilTest` |
+| P0: Transactional path | RESOLVED | 11 dedicated tests + `AbstractTransactionalOperatorTest` base class |
+| P0: `validateSession()` untested | RESOLVED | 2 tests in `MongoChangeTemplateTest` for apply and rollback paths |
+| P1: Options integration | RESOLVED | 9 end-to-end tests: bypass validation, ordered, TTL, sparse, partial filter, collation |
+| P2: DDL operator gaps | RESOLVED | 8 tests: drop idempotency, createIndex idempotency/conflict, modifyCollection gaps |
---
@@ -339,10 +354,10 @@ Only `validator`, `validationLevel`, and `validationAction` are supported. Missi
| Implementation Correctness | 15% | 9 | 1.35 |
| Validation & Error Handling | 20% | 8 | 1.60 |
| Template Feature Completeness | 15% | 6 | 0.90 |
-| Test Coverage | 20% | 7 | 1.40 |
+| Test Coverage | 20% | 8 | 1.60 |
| Security & Safety | 10% | 8 | 0.80 |
| Code Quality & Maintainability | 5% | 8 | 0.40 |
-| **Total** | **100%** | | **7.80 / 10** |
+| **Total** | **100%** | | **8.00 / 10** |
### Score Justification
@@ -354,7 +369,7 @@ Only `validator`, `validationLevel`, and `validationAction` are supported. Missi
**Template Feature Completeness (6/10):** The template covers 11 MongoDB operations, which handles the most common change scenarios. However, feature gaps reduce the "no-code" value proposition: `delete` lacks `options` support (inconsistent with insert/update), `createCollection` accepts zero parameters (no capped/timeseries collections), `replaceOne` is missing entirely (semantically different from `update`), `modifyCollection` only exposes 3 of many `collMod` options, and `dropView` has no safety check against accidentally dropping real collections. See section 8 for details.
-**Test Coverage (7/10):** Dramatically expanded from the original analysis. 94 validation tests cover all 11 operations with type checks, missing parameters, unrecognized parameter keys, unrecognized option keys, no-parameter enforcement, nested element type checks, and error accumulation. 82 mapper unit tests cover all option conversions including collation. 5 exception wrapping tests verify `MongoTemplateExecutionException` behavior. Operator tests expanded (UpdateOperatorTest: 7, DeleteOperatorTest: 6). 12 new idempotency tests cover all 4 DDL operators: CreateCollectionOperatorTest (+2), DropIndexOperatorTest (+4), CreateViewOperatorTest (+2), RenameCollectionOperatorTest (+4). However: zero transactional path tests, zero options-with-operator integration tests. ~237 total tests.
+**Test Coverage (8/10):** Comprehensive across all layers. 94 validation tests cover all 11 operations with type checks, missing parameters, unrecognized parameter keys, unrecognized option keys, no-parameter enforcement, nested element type checks, and error accumulation. 82 mapper unit tests cover all option conversions including collation. 5 exception wrapping tests verify `MongoTemplateExecutionException` behavior. 11 transactional path tests across 3 dedicated test classes exercise `ClientSession` commit, abort, and session-with-options branches for all 3 transactional operators. 9 options integration tests verify end-to-end behavior of `bypassDocumentValidation`, `ordered`, `expireAfterSeconds`, `sparse`, `partialFilterExpression`, and `collation`. 2 `validateSession()` tests confirm `IllegalArgumentException` for transactional changes with null session. DDL gap tests cover idempotency for dropCollection, dropView, createIndex, and modifyCollection (enforcement, metadata assertions, partial parameters, re-application). ~265 total tests. The remaining deduction is for missing end-to-end tests for `arrayFilters`, `collation` on createIndex, and full Flamingock pipeline tests with transactional changes.
**Security (8/10):** Developer-authored context makes injection-style concerns not applicable. Collection name `$`/`\0` checks serve as guardrails, now applied to both `collection` and `target` parameters. `modifyCollection` parameters are validated against known values. YAML deserialization is delegated to the framework.
@@ -362,7 +377,7 @@ Only `validator`, `validationLevel`, and `validationAction` are supported. Missi
### Bottom Line
-The module has a **solid architecture and clean codebase**, with comprehensive validation, error handling, and retry safety. All 10 original correctness issues, all 5 validation-to-execution gaps, all 3 silent validation gaps, and all 4 idempotency gaps have been resolved. MongoDB driver exceptions are now wrapped with template-level context for easier debugging. 9 of 11 operations are now idempotent — multi-step changes with non-transactional DDL operations are retry-safe. The remaining area for improvement is **feature gaps** — missing `replaceOne`, inconsistent options support, bare-bones `createCollection` (section 8). The module is **production-ready for simple and moderately complex changes** with robust multi-step retry behavior.
+The module has a **solid architecture and clean codebase**, with comprehensive validation, error handling, retry safety, and test coverage. All 10 original correctness issues, all 5 validation-to-execution gaps, all 3 silent validation gaps, all 4 idempotency gaps, and all test coverage gaps (transactional path, options integration, DDL operator gaps) have been resolved. MongoDB driver exceptions are now wrapped with template-level context for easier debugging. 9 of 11 operations are now idempotent — multi-step changes with non-transactional DDL operations are retry-safe. Test coverage spans 265 tests including transactional path verification, end-to-end options integration, and DDL idempotency. The remaining area for improvement is **feature gaps** — missing `replaceOne`, inconsistent options support, bare-bones `createCollection` (section 8). The module is **production-ready for simple and moderately complex changes** with robust multi-step retry behavior.
---
diff --git a/src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java b/src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java
index 6c2b51d..b4c10eb 100644
--- a/src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java
+++ b/src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java
@@ -45,6 +45,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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@EnableFlamingock(configFile = "flamingock/pipeline.yaml")
@@ -291,4 +292,40 @@ void frameworkTriggeredRollbackForSingleOperation() {
assertFalse(collectionExists("stepRollbackTest"),
"Collection should not exist after framework-triggered rollback");
}
+
+ @Test
+ @DisplayName("WHEN apply is invoked with transactional=true and null session THEN IllegalArgumentException is thrown")
+ void applyWithTransactionalChangeAndNullSessionThrowsTest() {
+ MongoChangeTemplate template = new MongoChangeTemplate();
+ template.setChangeId("txn-apply-test");
+ template.setTransactional(true);
+
+ MongoOperation createCollectionOp = new MongoOperation();
+ createCollectionOp.setType("createCollection");
+ createCollectionOp.setCollection("stepRollbackTest");
+ template.setApplyPayload(createCollectionOp);
+
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+ () -> template.apply(mongoDatabase, null));
+ assertTrue(ex.getMessage().contains("txn-apply-test"),
+ "Exception message should contain the changeId");
+ }
+
+ @Test
+ @DisplayName("WHEN rollback is invoked with transactional=true and null session THEN IllegalArgumentException is thrown")
+ void rollbackWithTransactionalChangeAndNullSessionThrowsTest() {
+ MongoChangeTemplate template = new MongoChangeTemplate();
+ template.setChangeId("txn-rollback-test");
+ template.setTransactional(true);
+
+ MongoOperation dropCollectionOp = new MongoOperation();
+ dropCollectionOp.setType("dropCollection");
+ dropCollectionOp.setCollection("stepRollbackTest");
+ template.setRollbackPayload(dropCollectionOp);
+
+ IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
+ () -> template.rollback(mongoDatabase, null));
+ assertTrue(ex.getMessage().contains("txn-rollback-test"),
+ "Exception message should contain the changeId");
+ }
}
diff --git a/src/test/java/io/flamingock/template/mongodb/operations/AbstractTransactionalOperatorTest.java b/src/test/java/io/flamingock/template/mongodb/operations/AbstractTransactionalOperatorTest.java
new file mode 100644
index 0000000..55d2100
--- /dev/null
+++ b/src/test/java/io/flamingock/template/mongodb/operations/AbstractTransactionalOperatorTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.client.ClientSession;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Base class for transactional operator tests. Extends {@link AbstractMongoOperatorTest}
+ * with session lifecycle management: starts a session and transaction before each test,
+ * and aborts any active transaction + closes the session after each test.
+ *
+ *
TestContainers 2.x with {@code mongo:6} starts a single-node replica set by default,
+ * so transactions work without additional container configuration.
+ */
+abstract class AbstractTransactionalOperatorTest extends AbstractMongoOperatorTest {
+
+ protected ClientSession clientSession;
+
+ @BeforeEach
+ void setupSession() {
+ clientSession = mongoClient.startSession();
+ clientSession.startTransaction();
+ }
+
+ @AfterEach
+ void teardownSession() {
+ if (clientSession != null) {
+ if (clientSession.hasActiveTransaction()) {
+ clientSession.abortTransaction();
+ }
+ clientSession.close();
+ }
+ }
+}
diff --git a/src/test/java/io/flamingock/template/mongodb/operations/CreateIndexOperatorOptionsTest.java b/src/test/java/io/flamingock/template/mongodb/operations/CreateIndexOperatorOptionsTest.java
new file mode 100644
index 0000000..b669116
--- /dev/null
+++ b/src/test/java/io/flamingock/template/mongodb/operations/CreateIndexOperatorOptionsTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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 io.flamingock.template.mongodb.model.MongoOperation;
+import io.flamingock.template.mongodb.model.operator.CreateIndexOperator;
+import org.bson.Document;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class CreateIndexOperatorOptionsTest extends AbstractMongoOperatorTest {
+
+ private static final String COLLECTION_NAME = "indexOptionsTestCollection";
+
+ @BeforeEach
+ void setupEach() {
+ mongoDatabase.getCollection(COLLECTION_NAME).drop();
+ mongoDatabase.createCollection(COLLECTION_NAME);
+ }
+
+ @Test
+ @DisplayName("WHEN createIndex with expireAfterSeconds THEN TTL index is created")
+ void createTTLIndexTest() {
+ MongoOperation operation = new MongoOperation();
+ operation.setType("createIndex");
+ operation.setCollection(COLLECTION_NAME);
+
+ Map params = new HashMap<>();
+ Map keys = new HashMap<>();
+ keys.put("createdAt", 1);
+ params.put("keys", keys);
+
+ Map options = new HashMap<>();
+ options.put("expireAfterSeconds", 3600);
+ options.put("name", "ttl_index");
+ params.put("options", options);
+ operation.setParameters(params);
+
+ new CreateIndexOperator(mongoDatabase, operation).apply(null);
+
+ Document index = findIndexByName("ttl_index");
+ assertNotNull(index, "TTL index should exist");
+ Number expireAfterSeconds = (Number) index.get("expireAfterSeconds");
+ assertNotNull(expireAfterSeconds, "Index should have expireAfterSeconds");
+ assertEquals(3600, expireAfterSeconds.intValue(),
+ "Index should have expireAfterSeconds=3600");
+ }
+
+ @Test
+ @DisplayName("WHEN createIndex with sparse=true THEN sparse index is created")
+ void createSparseIndexTest() {
+ MongoOperation operation = new MongoOperation();
+ operation.setType("createIndex");
+ operation.setCollection(COLLECTION_NAME);
+
+ Map params = new HashMap<>();
+ Map keys = new HashMap<>();
+ keys.put("optionalField", 1);
+ params.put("keys", keys);
+
+ Map options = new HashMap<>();
+ options.put("sparse", true);
+ options.put("name", "sparse_index");
+ params.put("options", options);
+ operation.setParameters(params);
+
+ new CreateIndexOperator(mongoDatabase, operation).apply(null);
+
+ Document index = findIndexByName("sparse_index");
+ assertNotNull(index, "Sparse index should exist");
+ assertTrue(Boolean.TRUE.equals(index.getBoolean("sparse")),
+ "Index should be sparse");
+ }
+
+ @Test
+ @DisplayName("WHEN createIndex with partialFilterExpression THEN partial index is created")
+ void createIndexWithPartialFilterExpressionTest() {
+ 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("name", "partial_filter_index");
+ Map partialFilter = new HashMap<>();
+ partialFilter.put("status", "active");
+ options.put("partialFilterExpression", partialFilter);
+ params.put("options", options);
+ operation.setParameters(params);
+
+ new CreateIndexOperator(mongoDatabase, operation).apply(null);
+
+ Document index = findIndexByName("partial_filter_index");
+ assertNotNull(index, "Partial filter index should exist");
+ Document partialFilterExpression = index.get("partialFilterExpression", Document.class);
+ assertNotNull(partialFilterExpression, "Index should have partialFilterExpression");
+ assertEquals("active", partialFilterExpression.getString("status"),
+ "Partial filter should match the configured expression");
+ }
+
+ private Document findIndexByName(String indexName) {
+ List indexes = mongoDatabase.getCollection(COLLECTION_NAME)
+ .listIndexes()
+ .into(new ArrayList<>());
+ return indexes.stream()
+ .filter(idx -> indexName.equals(idx.getString("name")))
+ .findFirst()
+ .orElse(null);
+ }
+}
diff --git a/src/test/java/io/flamingock/template/mongodb/operations/CreateIndexOperatorTest.java b/src/test/java/io/flamingock/template/mongodb/operations/CreateIndexOperatorTest.java
index bd211cc..dede225 100644
--- a/src/test/java/io/flamingock/template/mongodb/operations/CreateIndexOperatorTest.java
+++ b/src/test/java/io/flamingock/template/mongodb/operations/CreateIndexOperatorTest.java
@@ -27,6 +27,10 @@
import java.util.List;
import java.util.Map;
+import io.flamingock.template.mongodb.MongoTemplateExecutionException;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CreateIndexOperatorTest extends AbstractMongoOperatorTest {
@@ -105,6 +109,78 @@ private boolean indexExistsByKeys(String keyField) {
});
}
+ @Test
+ @DisplayName("WHEN identical index is created twice THEN no exception is thrown (idempotent)")
+ void createIdenticalIndexIdempotentTest() {
+ 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);
+
+ // First creation
+ new CreateIndexOperator(mongoDatabase, operation).apply(null);
+ assertTrue(indexExists(INDEX_NAME), "Index should exist after first creation");
+
+ // Second identical creation — should be idempotent
+ assertDoesNotThrow(() -> new CreateIndexOperator(mongoDatabase, operation).apply(null),
+ "Creating identical index should not throw");
+ assertTrue(indexExists(INDEX_NAME), "Index should still exist after second creation");
+ }
+
+ @Test
+ @DisplayName("WHEN index with same name but different keys is created THEN MongoTemplateExecutionException is thrown")
+ void createConflictingIndexThrowsTest() {
+ mongoDatabase.createCollection(COLLECTION_NAME);
+
+ // First: create index on email with a specific name
+ MongoOperation operation1 = new MongoOperation();
+ operation1.setType("createIndex");
+ operation1.setCollection(COLLECTION_NAME);
+
+ Map params1 = new HashMap<>();
+ Map keys1 = new HashMap<>();
+ keys1.put("email", 1);
+ params1.put("keys", keys1);
+
+ Map options1 = new HashMap<>();
+ options1.put("name", "my_index");
+ params1.put("options", options1);
+ operation1.setParameters(params1);
+
+ new CreateIndexOperator(mongoDatabase, operation1).apply(null);
+
+ // Second: create index on a DIFFERENT key but with the SAME name — guaranteed conflict
+ MongoOperation operation2 = new MongoOperation();
+ operation2.setType("createIndex");
+ operation2.setCollection(COLLECTION_NAME);
+
+ Map params2 = new HashMap<>();
+ Map keys2 = new HashMap<>();
+ keys2.put("name", 1);
+ params2.put("keys", keys2);
+
+ Map options2 = new HashMap<>();
+ options2.put("name", "my_index");
+ params2.put("options", options2);
+ operation2.setParameters(params2);
+
+ assertThrows(MongoTemplateExecutionException.class,
+ () -> new CreateIndexOperator(mongoDatabase, operation2).apply(null),
+ "Creating index with same name but different keys should throw");
+ }
+
private boolean isIndexUnique(String indexName) {
List indexes = mongoDatabase.getCollection(COLLECTION_NAME)
.listIndexes()
diff --git a/src/test/java/io/flamingock/template/mongodb/operations/DeleteOperatorTransactionalTest.java b/src/test/java/io/flamingock/template/mongodb/operations/DeleteOperatorTransactionalTest.java
new file mode 100644
index 0000000..a5df02f
--- /dev/null
+++ b/src/test/java/io/flamingock/template/mongodb/operations/DeleteOperatorTransactionalTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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 io.flamingock.template.mongodb.model.MongoOperation;
+import io.flamingock.template.mongodb.model.operator.DeleteOperator;
+import org.bson.Document;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class DeleteOperatorTransactionalTest extends AbstractTransactionalOperatorTest {
+
+ private static final String COLLECTION_NAME = "deleteTxnTestCollection";
+
+ @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 deleteOne is applied with session THEN one document is deleted after commit")
+ void deleteOneWithSessionTest() {
+ assertEquals(4, getDocumentCount(), "Collection should have 4 documents before delete");
+
+ MongoOperation operation = buildDeleteOperation("role", "user", false);
+ new DeleteOperator(mongoDatabase, operation).apply(clientSession);
+
+ clientSession.commitTransaction();
+
+ assertEquals(3, getDocumentCount(), "One document should be deleted after commit");
+ }
+
+ @Test
+ @DisplayName("WHEN deleteMany is applied with session THEN all matching documents are deleted after commit")
+ void deleteManyWithSessionTest() {
+ assertEquals(4, getDocumentCount(), "Collection should have 4 documents before delete");
+
+ MongoOperation operation = buildDeleteOperation("role", "user", true);
+ new DeleteOperator(mongoDatabase, operation).apply(clientSession);
+
+ clientSession.commitTransaction();
+
+ assertEquals(2, getDocumentCount(), "Two user documents should be deleted after commit");
+
+ List remaining = mongoDatabase.getCollection(COLLECTION_NAME)
+ .find()
+ .into(new ArrayList<>());
+ for (Document doc : remaining) {
+ assertEquals("admin", doc.getString("role"), "Only admin documents should remain");
+ }
+ }
+
+ @Test
+ @DisplayName("WHEN delete is applied with session and transaction is aborted THEN documents are unchanged")
+ void deleteWithSessionAbortRollsBackTest() {
+ assertEquals(4, getDocumentCount(), "Collection should have 4 documents before delete");
+
+ MongoOperation operation = buildDeleteOperation("role", "user", true);
+ new DeleteOperator(mongoDatabase, operation).apply(clientSession);
+
+ clientSession.abortTransaction();
+
+ assertEquals(4, getDocumentCount(),
+ "All documents should still exist after abort — proves session is passed through");
+ }
+
+ private MongoOperation buildDeleteOperation(String filterKey, Object filterValue, boolean multi) {
+ MongoOperation operation = new MongoOperation();
+ operation.setType("delete");
+ operation.setCollection(COLLECTION_NAME);
+
+ Map params = new HashMap<>();
+ Map filter = new HashMap<>();
+ filter.put(filterKey, filterValue);
+ params.put("filter", filter);
+ if (multi) {
+ params.put("multi", true);
+ }
+ operation.setParameters(params);
+ return operation;
+ }
+
+ private long getDocumentCount() {
+ return mongoDatabase.getCollection(COLLECTION_NAME).countDocuments();
+ }
+}
diff --git a/src/test/java/io/flamingock/template/mongodb/operations/DropCollectionOperatorTest.java b/src/test/java/io/flamingock/template/mongodb/operations/DropCollectionOperatorTest.java
index 3194c7e..cb33a73 100644
--- a/src/test/java/io/flamingock/template/mongodb/operations/DropCollectionOperatorTest.java
+++ b/src/test/java/io/flamingock/template/mongodb/operations/DropCollectionOperatorTest.java
@@ -53,6 +53,23 @@ void dropCollectionTest() {
assertFalse(collectionExists(COLLECTION_NAME), "Collection should have been dropped");
}
+ @Test
+ @DisplayName("WHEN dropCollection is applied on non-existent collection THEN no exception is thrown")
+ void dropNonExistentCollectionSucceedsSilentlyTest() {
+ assertFalse(collectionExists(COLLECTION_NAME), "Collection should not exist before drop");
+
+ MongoOperation operation = new MongoOperation();
+ operation.setType("dropCollection");
+ operation.setCollection(COLLECTION_NAME);
+ operation.setParameters(new HashMap<>());
+
+ DropCollectionOperator operator = new DropCollectionOperator(mongoDatabase, operation);
+ // MongoDB drop() is natively idempotent — no exception on non-existent collection
+ operator.apply(null);
+
+ assertFalse(collectionExists(COLLECTION_NAME), "Collection should still not exist");
+ }
+
private boolean collectionExists(String collectionName) {
return mongoDatabase.listCollectionNames()
.into(new ArrayList<>())
diff --git a/src/test/java/io/flamingock/template/mongodb/operations/DropViewOperatorTest.java b/src/test/java/io/flamingock/template/mongodb/operations/DropViewOperatorTest.java
index b5a4923..8997433 100644
--- a/src/test/java/io/flamingock/template/mongodb/operations/DropViewOperatorTest.java
+++ b/src/test/java/io/flamingock/template/mongodb/operations/DropViewOperatorTest.java
@@ -58,6 +58,23 @@ void dropViewTest() {
assertFalse(collectionExists(VIEW_NAME), "View should have been dropped");
}
+ @Test
+ @DisplayName("WHEN dropView is applied on non-existent view THEN no exception is thrown")
+ void dropNonExistentViewSucceedsSilentlyTest() {
+ assertFalse(collectionExists(VIEW_NAME), "View should not exist before drop");
+
+ MongoOperation operation = new MongoOperation();
+ operation.setType("dropView");
+ operation.setCollection(VIEW_NAME);
+ operation.setParameters(new HashMap<>());
+
+ DropViewOperator operator = new DropViewOperator(mongoDatabase, operation);
+ // MongoDB drop() is natively idempotent — no exception on non-existent view
+ operator.apply(null);
+
+ assertFalse(collectionExists(VIEW_NAME), "View should still not exist");
+ }
+
private boolean collectionExists(String collectionName) {
List collections = mongoDatabase.listCollectionNames().into(new ArrayList<>());
return collections.contains(collectionName);
diff --git a/src/test/java/io/flamingock/template/mongodb/operations/InsertOperatorOptionsTest.java b/src/test/java/io/flamingock/template/mongodb/operations/InsertOperatorOptionsTest.java
new file mode 100644
index 0000000..88bd0c7
--- /dev/null
+++ b/src/test/java/io/flamingock/template/mongodb/operations/InsertOperatorOptionsTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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 io.flamingock.template.mongodb.MongoTemplateExecutionException;
+import io.flamingock.template.mongodb.model.MongoOperation;
+import io.flamingock.template.mongodb.model.operator.InsertOperator;
+import org.bson.Document;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+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.assertThrows;
+
+class InsertOperatorOptionsTest extends AbstractMongoOperatorTest {
+
+ private static final String COLLECTION_NAME = "insertOptionsTestCollection";
+
+ @BeforeEach
+ void setupEach() {
+ mongoDatabase.getCollection(COLLECTION_NAME).drop();
+ }
+
+ @Test
+ @DisplayName("WHEN insert with bypassDocumentValidation=true THEN document bypasses schema validator")
+ void insertWithBypassDocumentValidationTest() {
+ // Create collection with JSON schema validator requiring 'name' and 'email'
+ Document validator = new Document("$jsonSchema",
+ new Document("bsonType", "object")
+ .append("required", java.util.Arrays.asList("name", "email"))
+ .append("properties", new Document("name",
+ new Document("bsonType", "string"))
+ .append("email", new Document("bsonType", "string"))));
+
+ mongoDatabase.runCommand(new Document("create", COLLECTION_NAME)
+ .append("validator", validator)
+ .append("validationAction", "error"));
+
+ // Insert a document missing 'email' — should succeed with bypassDocumentValidation=true
+ MongoOperation operation = new MongoOperation();
+ operation.setType("insert");
+ operation.setCollection(COLLECTION_NAME);
+
+ Map params = new HashMap<>();
+ List