Skip to content

Commit c925833

Browse files
authored
test: address test coverage gaps from ANALYSIS.md section 4.2 (#18)
Add 28 new tests covering transactional path (P0), options integration (P1), and DDL operator gaps (P2): - Transactional path: 11 tests across InsertOperatorTransactionalTest, UpdateOperatorTransactionalTest, DeleteOperatorTransactionalTest exercising ClientSession commit, abort, and session-with-options branches. AbstractTransactionalOperatorTest provides shared lifecycle. - validateSession: 2 tests in MongoChangeTemplateTest verify IllegalArgumentException for transactional changes with null session. - Options integration: 9 end-to-end tests for bypassDocumentValidation, ordered, expireAfterSeconds, sparse, partialFilterExpression, and collation with case-insensitive matching. - DDL gaps: 8 tests for drop idempotency, createIndex idempotency/conflict, and modifyCollection enforcement, metadata assertions, partial parameters, and re-application. Update ANALYSIS.md sections 4.1, 4.2, and 10 to reflect resolved gaps and updated test coverage score (7→8, total 7.80→8.00).
1 parent e1c550c commit c925833

13 files changed

Lines changed: 1133 additions & 29 deletions

ANALYSIS.md

Lines changed: 44 additions & 29 deletions
Large diffs are not rendered by default.

src/test/java/io/flamingock/template/mongodb/MongoChangeTemplateTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME;
4646
import static org.junit.jupiter.api.Assertions.assertEquals;
4747
import static org.junit.jupiter.api.Assertions.assertFalse;
48+
import static org.junit.jupiter.api.Assertions.assertThrows;
4849
import static org.junit.jupiter.api.Assertions.assertTrue;
4950

5051
@EnableFlamingock(configFile = "flamingock/pipeline.yaml")
@@ -291,4 +292,40 @@ void frameworkTriggeredRollbackForSingleOperation() {
291292
assertFalse(collectionExists("stepRollbackTest"),
292293
"Collection should not exist after framework-triggered rollback");
293294
}
295+
296+
@Test
297+
@DisplayName("WHEN apply is invoked with transactional=true and null session THEN IllegalArgumentException is thrown")
298+
void applyWithTransactionalChangeAndNullSessionThrowsTest() {
299+
MongoChangeTemplate template = new MongoChangeTemplate();
300+
template.setChangeId("txn-apply-test");
301+
template.setTransactional(true);
302+
303+
MongoOperation createCollectionOp = new MongoOperation();
304+
createCollectionOp.setType("createCollection");
305+
createCollectionOp.setCollection("stepRollbackTest");
306+
template.setApplyPayload(createCollectionOp);
307+
308+
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
309+
() -> template.apply(mongoDatabase, null));
310+
assertTrue(ex.getMessage().contains("txn-apply-test"),
311+
"Exception message should contain the changeId");
312+
}
313+
314+
@Test
315+
@DisplayName("WHEN rollback is invoked with transactional=true and null session THEN IllegalArgumentException is thrown")
316+
void rollbackWithTransactionalChangeAndNullSessionThrowsTest() {
317+
MongoChangeTemplate template = new MongoChangeTemplate();
318+
template.setChangeId("txn-rollback-test");
319+
template.setTransactional(true);
320+
321+
MongoOperation dropCollectionOp = new MongoOperation();
322+
dropCollectionOp.setType("dropCollection");
323+
dropCollectionOp.setCollection("stepRollbackTest");
324+
template.setRollbackPayload(dropCollectionOp);
325+
326+
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
327+
() -> template.rollback(mongoDatabase, null));
328+
assertTrue(ex.getMessage().contains("txn-rollback-test"),
329+
"Exception message should contain the changeId");
330+
}
294331
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2025 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.template.mongodb.operations;
17+
18+
import com.mongodb.client.ClientSession;
19+
import org.junit.jupiter.api.AfterEach;
20+
import org.junit.jupiter.api.BeforeEach;
21+
22+
/**
23+
* Base class for transactional operator tests. Extends {@link AbstractMongoOperatorTest}
24+
* with session lifecycle management: starts a session and transaction before each test,
25+
* and aborts any active transaction + closes the session after each test.
26+
*
27+
* <p>TestContainers 2.x with {@code mongo:6} starts a single-node replica set by default,
28+
* so transactions work without additional container configuration.</p>
29+
*/
30+
abstract class AbstractTransactionalOperatorTest extends AbstractMongoOperatorTest {
31+
32+
protected ClientSession clientSession;
33+
34+
@BeforeEach
35+
void setupSession() {
36+
clientSession = mongoClient.startSession();
37+
clientSession.startTransaction();
38+
}
39+
40+
@AfterEach
41+
void teardownSession() {
42+
if (clientSession != null) {
43+
if (clientSession.hasActiveTransaction()) {
44+
clientSession.abortTransaction();
45+
}
46+
clientSession.close();
47+
}
48+
}
49+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright 2025 Flamingock (https://www.flamingock.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.flamingock.template.mongodb.operations;
17+
18+
import io.flamingock.template.mongodb.model.MongoOperation;
19+
import io.flamingock.template.mongodb.model.operator.CreateIndexOperator;
20+
import org.bson.Document;
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.DisplayName;
23+
import org.junit.jupiter.api.Test;
24+
25+
import java.util.ArrayList;
26+
import java.util.HashMap;
27+
import java.util.List;
28+
import java.util.Map;
29+
30+
import static org.junit.jupiter.api.Assertions.assertEquals;
31+
import static org.junit.jupiter.api.Assertions.assertNotNull;
32+
import static org.junit.jupiter.api.Assertions.assertTrue;
33+
34+
class CreateIndexOperatorOptionsTest extends AbstractMongoOperatorTest {
35+
36+
private static final String COLLECTION_NAME = "indexOptionsTestCollection";
37+
38+
@BeforeEach
39+
void setupEach() {
40+
mongoDatabase.getCollection(COLLECTION_NAME).drop();
41+
mongoDatabase.createCollection(COLLECTION_NAME);
42+
}
43+
44+
@Test
45+
@DisplayName("WHEN createIndex with expireAfterSeconds THEN TTL index is created")
46+
void createTTLIndexTest() {
47+
MongoOperation operation = new MongoOperation();
48+
operation.setType("createIndex");
49+
operation.setCollection(COLLECTION_NAME);
50+
51+
Map<String, Object> params = new HashMap<>();
52+
Map<String, Object> keys = new HashMap<>();
53+
keys.put("createdAt", 1);
54+
params.put("keys", keys);
55+
56+
Map<String, Object> options = new HashMap<>();
57+
options.put("expireAfterSeconds", 3600);
58+
options.put("name", "ttl_index");
59+
params.put("options", options);
60+
operation.setParameters(params);
61+
62+
new CreateIndexOperator(mongoDatabase, operation).apply(null);
63+
64+
Document index = findIndexByName("ttl_index");
65+
assertNotNull(index, "TTL index should exist");
66+
Number expireAfterSeconds = (Number) index.get("expireAfterSeconds");
67+
assertNotNull(expireAfterSeconds, "Index should have expireAfterSeconds");
68+
assertEquals(3600, expireAfterSeconds.intValue(),
69+
"Index should have expireAfterSeconds=3600");
70+
}
71+
72+
@Test
73+
@DisplayName("WHEN createIndex with sparse=true THEN sparse index is created")
74+
void createSparseIndexTest() {
75+
MongoOperation operation = new MongoOperation();
76+
operation.setType("createIndex");
77+
operation.setCollection(COLLECTION_NAME);
78+
79+
Map<String, Object> params = new HashMap<>();
80+
Map<String, Object> keys = new HashMap<>();
81+
keys.put("optionalField", 1);
82+
params.put("keys", keys);
83+
84+
Map<String, Object> options = new HashMap<>();
85+
options.put("sparse", true);
86+
options.put("name", "sparse_index");
87+
params.put("options", options);
88+
operation.setParameters(params);
89+
90+
new CreateIndexOperator(mongoDatabase, operation).apply(null);
91+
92+
Document index = findIndexByName("sparse_index");
93+
assertNotNull(index, "Sparse index should exist");
94+
assertTrue(Boolean.TRUE.equals(index.getBoolean("sparse")),
95+
"Index should be sparse");
96+
}
97+
98+
@Test
99+
@DisplayName("WHEN createIndex with partialFilterExpression THEN partial index is created")
100+
void createIndexWithPartialFilterExpressionTest() {
101+
MongoOperation operation = new MongoOperation();
102+
operation.setType("createIndex");
103+
operation.setCollection(COLLECTION_NAME);
104+
105+
Map<String, Object> params = new HashMap<>();
106+
Map<String, Object> keys = new HashMap<>();
107+
keys.put("email", 1);
108+
params.put("keys", keys);
109+
110+
Map<String, Object> options = new HashMap<>();
111+
options.put("name", "partial_filter_index");
112+
Map<String, Object> partialFilter = new HashMap<>();
113+
partialFilter.put("status", "active");
114+
options.put("partialFilterExpression", partialFilter);
115+
params.put("options", options);
116+
operation.setParameters(params);
117+
118+
new CreateIndexOperator(mongoDatabase, operation).apply(null);
119+
120+
Document index = findIndexByName("partial_filter_index");
121+
assertNotNull(index, "Partial filter index should exist");
122+
Document partialFilterExpression = index.get("partialFilterExpression", Document.class);
123+
assertNotNull(partialFilterExpression, "Index should have partialFilterExpression");
124+
assertEquals("active", partialFilterExpression.getString("status"),
125+
"Partial filter should match the configured expression");
126+
}
127+
128+
private Document findIndexByName(String indexName) {
129+
List<Document> indexes = mongoDatabase.getCollection(COLLECTION_NAME)
130+
.listIndexes()
131+
.into(new ArrayList<>());
132+
return indexes.stream()
133+
.filter(idx -> indexName.equals(idx.getString("name")))
134+
.findFirst()
135+
.orElse(null);
136+
}
137+
}

src/test/java/io/flamingock/template/mongodb/operations/CreateIndexOperatorTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
import java.util.List;
2828
import java.util.Map;
2929

30+
import io.flamingock.template.mongodb.MongoTemplateExecutionException;
31+
32+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
33+
import static org.junit.jupiter.api.Assertions.assertThrows;
3034
import static org.junit.jupiter.api.Assertions.assertTrue;
3135

3236
class CreateIndexOperatorTest extends AbstractMongoOperatorTest {
@@ -105,6 +109,78 @@ private boolean indexExistsByKeys(String keyField) {
105109
});
106110
}
107111

112+
@Test
113+
@DisplayName("WHEN identical index is created twice THEN no exception is thrown (idempotent)")
114+
void createIdenticalIndexIdempotentTest() {
115+
mongoDatabase.createCollection(COLLECTION_NAME);
116+
117+
MongoOperation operation = new MongoOperation();
118+
operation.setType("createIndex");
119+
operation.setCollection(COLLECTION_NAME);
120+
121+
Map<String, Object> params = new HashMap<>();
122+
Map<String, Object> keys = new HashMap<>();
123+
keys.put("email", 1);
124+
params.put("keys", keys);
125+
126+
Map<String, Object> options = new HashMap<>();
127+
options.put("unique", true);
128+
options.put("name", INDEX_NAME);
129+
params.put("options", options);
130+
operation.setParameters(params);
131+
132+
// First creation
133+
new CreateIndexOperator(mongoDatabase, operation).apply(null);
134+
assertTrue(indexExists(INDEX_NAME), "Index should exist after first creation");
135+
136+
// Second identical creation — should be idempotent
137+
assertDoesNotThrow(() -> new CreateIndexOperator(mongoDatabase, operation).apply(null),
138+
"Creating identical index should not throw");
139+
assertTrue(indexExists(INDEX_NAME), "Index should still exist after second creation");
140+
}
141+
142+
@Test
143+
@DisplayName("WHEN index with same name but different keys is created THEN MongoTemplateExecutionException is thrown")
144+
void createConflictingIndexThrowsTest() {
145+
mongoDatabase.createCollection(COLLECTION_NAME);
146+
147+
// First: create index on email with a specific name
148+
MongoOperation operation1 = new MongoOperation();
149+
operation1.setType("createIndex");
150+
operation1.setCollection(COLLECTION_NAME);
151+
152+
Map<String, Object> params1 = new HashMap<>();
153+
Map<String, Object> keys1 = new HashMap<>();
154+
keys1.put("email", 1);
155+
params1.put("keys", keys1);
156+
157+
Map<String, Object> options1 = new HashMap<>();
158+
options1.put("name", "my_index");
159+
params1.put("options", options1);
160+
operation1.setParameters(params1);
161+
162+
new CreateIndexOperator(mongoDatabase, operation1).apply(null);
163+
164+
// Second: create index on a DIFFERENT key but with the SAME name — guaranteed conflict
165+
MongoOperation operation2 = new MongoOperation();
166+
operation2.setType("createIndex");
167+
operation2.setCollection(COLLECTION_NAME);
168+
169+
Map<String, Object> params2 = new HashMap<>();
170+
Map<String, Object> keys2 = new HashMap<>();
171+
keys2.put("name", 1);
172+
params2.put("keys", keys2);
173+
174+
Map<String, Object> options2 = new HashMap<>();
175+
options2.put("name", "my_index");
176+
params2.put("options", options2);
177+
operation2.setParameters(params2);
178+
179+
assertThrows(MongoTemplateExecutionException.class,
180+
() -> new CreateIndexOperator(mongoDatabase, operation2).apply(null),
181+
"Creating index with same name but different keys should throw");
182+
}
183+
108184
private boolean isIndexUnique(String indexName) {
109185
List<Document> indexes = mongoDatabase.getCollection(COLLECTION_NAME)
110186
.listIndexes()

0 commit comments

Comments
 (0)