Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 44 additions & 29 deletions ANALYSIS.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>TestContainers 2.x with {@code mongo:6} starts a single-node replica set by default,
* so transactions work without additional container configuration.</p>
*/
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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> params = new HashMap<>();
Map<String, Object> keys = new HashMap<>();
keys.put("createdAt", 1);
params.put("keys", keys);

Map<String, Object> 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<String, Object> params = new HashMap<>();
Map<String, Object> keys = new HashMap<>();
keys.put("optionalField", 1);
params.put("keys", keys);

Map<String, Object> 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<String, Object> params = new HashMap<>();
Map<String, Object> keys = new HashMap<>();
keys.put("email", 1);
params.put("keys", keys);

Map<String, Object> options = new HashMap<>();
options.put("name", "partial_filter_index");
Map<String, Object> 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<Document> indexes = mongoDatabase.getCollection(COLLECTION_NAME)
.listIndexes()
.into(new ArrayList<>());
return indexes.stream()
.filter(idx -> indexName.equals(idx.getString("name")))
.findFirst()
.orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<String, Object> params = new HashMap<>();
Map<String, Object> keys = new HashMap<>();
keys.put("email", 1);
params.put("keys", keys);

Map<String, Object> 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<String, Object> params1 = new HashMap<>();
Map<String, Object> keys1 = new HashMap<>();
keys1.put("email", 1);
params1.put("keys", keys1);

Map<String, Object> 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<String, Object> params2 = new HashMap<>();
Map<String, Object> keys2 = new HashMap<>();
keys2.put("name", 1);
params2.put("keys", keys2);

Map<String, Object> 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<Document> indexes = mongoDatabase.getCollection(COLLECTION_NAME)
.listIndexes()
Expand Down
Loading
Loading