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
11 changes: 5 additions & 6 deletions ANALYSIS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@ YAML -> MongoOperation (deserialized) -> MongoOperationValidator -> MongoOperati
**Original issue:** `InsertOperator.applyInternal()` had a redundant guard `if(op.getDocuments() == null || op.getDocuments().isEmpty()) { return; }` that silently did nothing instead of failing when documents were missing.
**Resolution:** The silent guard was removed from `InsertOperator.applyInternal()`. Structural validation now runs at load time via `InsertParametersValidator` (called from `MongoOperation.validate()`), which catches null, empty, and malformed documents before any change executes. The operator no longer needs a defensive check — if documents are invalid, the framework rejects the change at load time.

### #3 - HIGH: `modifyCollection` has ZERO parameter validation
**File:** `MongoOperationValidator.java:239-240`
**Impact:** The `validateByType()` switch falls through to `default: return new ArrayList<>()` for `MODIFY_COLLECTION`, `DROP_COLLECTION`, `DROP_VIEW`, and `CREATE_COLLECTION`. While the last three genuinely need only a collection name, `modifyCollection` accepts `validator`, `validationLevel`, and `validationAction` parameters. None are validated. A user could pass `validationLevel: "banana"` and it would pass validation, only to fail at MongoDB runtime.
**Risk:** Silent misconfiguration of collection-level validation rules in production.
**Fix:** Add `case MODIFY_COLLECTION: return validateModifyCollection(op, entityId);` with checks for valid `validationLevel` values (`off`, `strict`, `moderate`) and valid `validationAction` values (`error`, `warn`).
### #3 - ~~HIGH: `modifyCollection` has ZERO parameter validation~~ RESOLVED
**Status:** Fixed by adding `ModifyCollectionParametersValidator`.
**Original issue:** `modifyCollection` used `OperationValidator.NO_OP` in `MongoOperationType`, so none of its three parameters (`validator`, `validationLevel`, `validationAction`) were validated. A user could pass `validationLevel: "banana"` and it would pass validation, only to fail at MongoDB runtime.
**Resolution:** `ModifyCollectionParametersValidator` now validates at load time: requires at least one recognized parameter, checks `validator` is a document (Map), checks `validationLevel` is one of `off`/`strict`/`moderate`, and checks `validationAction` is one of `error`/`warn`. Multiple errors accumulate. Wired into `MongoOperationType.MODIFY_COLLECTION` replacing the `NO_OP` validator.

### #4 - HIGH: Type-unsafe parameter extraction throughout MongoOperation
**File:** `MongoOperation.java:46-115`
Expand Down Expand Up @@ -103,7 +102,7 @@ While the validator catches some of these cases, the validator runs only on the
| createIndex | `CREATE_INDEX` | `CreateIndexOperator` | **Yes (wrong)** | Full (keys) | `IndexOptionsMapper` | **Warns & ignores** | `CreateIndexOperatorTest` (1 test) | YAML `_0003`, `_0005` |
| dropIndex | `DROP_INDEX` | `DropIndexOperator` | No | indexName or keys | None | **Ignores entirely** | `DropIndexOperatorTest` (1 test) | YAML `_0005` rollback |
| renameCollection | `RENAME_COLLECTION` | `RenameCollectionOperator` | No | target required | `RenameCollectionOptionsMapper` | Ignores entirely | `RenameCollectionOperatorTest` (1 test) | None |
| modifyCollection | `MODIFY_COLLECTION` | `ModifyCollectionOperator` | No | **None (bug)** | None | Ignores entirely | `ModifyCollectionOperatorTest` (1 test) | None |
| modifyCollection | `MODIFY_COLLECTION` | `ModifyCollectionOperator` | No | Full (validator, validationLevel, validationAction) | None | Ignores entirely | `ModifyCollectionOperatorTest` (1 test) | None |
| createView | `CREATE_VIEW` | `CreateViewOperator` | No | Full (viewOn, pipeline) | `CreateViewOptionsMapper` | Ignores entirely | `CreateViewOperatorTest` (1 test) | None |
| dropView | `DROP_VIEW` | `DropViewOperator` | No | collection only | None | Ignores entirely | `DropViewOperatorTest` (1 test) | None |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.flamingock.api.annotations.Nullable;
import io.flamingock.api.annotations.Rollback;
import io.flamingock.api.template.AbstractChangeTemplate;
import io.flamingock.api.template.wrappers.TemplateVoid;
import io.flamingock.template.mongodb.model.MongoOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -73,7 +74,7 @@
* @see MongoOperation
*/
@ChangeTemplate( name = "mongodb-sync-template", multiStep = true)
public class MongoChangeTemplate extends AbstractChangeTemplate<Void, MongoOperation, MongoOperation> {
public class MongoChangeTemplate extends AbstractChangeTemplate<TemplateVoid, MongoOperation, MongoOperation> {

private static final Logger log = LoggerFactory.getLogger(MongoChangeTemplate.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.flamingock.template.mongodb.validation.DeleteParametersValidator;
import io.flamingock.template.mongodb.validation.DropIndexParametersValidator;
import io.flamingock.template.mongodb.validation.InsertParametersValidator;
import io.flamingock.template.mongodb.validation.ModifyCollectionParametersValidator;
import io.flamingock.template.mongodb.validation.OperationValidator;
import io.flamingock.template.mongodb.validation.RenameCollectionParametersValidator;
import io.flamingock.template.mongodb.validation.UpdateParametersValidator;
Expand All @@ -51,7 +52,7 @@ public enum MongoOperationType {
DROP_COLLECTION("dropCollection", DropCollectionOperator::new, OperationValidator.NO_OP),
DROP_INDEX("dropIndex", DropIndexOperator::new, new DropIndexParametersValidator()),
RENAME_COLLECTION("renameCollection", RenameCollectionOperator::new, new RenameCollectionParametersValidator()),
MODIFY_COLLECTION("modifyCollection", ModifyCollectionOperator::new, OperationValidator.NO_OP),
MODIFY_COLLECTION("modifyCollection", ModifyCollectionOperator::new, new ModifyCollectionParametersValidator()),
CREATE_VIEW("createView", CreateViewOperator::new, new CreateViewParametersValidator()),
DROP_VIEW("dropView", DropViewOperator::new, OperationValidator.NO_OP);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2026 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.validation;

import io.flamingock.api.template.TemplatePayloadValidationError;
import io.flamingock.template.mongodb.model.MongoOperation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class ModifyCollectionParametersValidator implements OperationValidator {

private static final Set<String> VALID_VALIDATION_LEVELS = new HashSet<>(Arrays.asList("off", "strict", "moderate"));
private static final Set<String> VALID_VALIDATION_ACTIONS = new HashSet<>(Arrays.asList("error", "warn"));
private static final Set<String> RECOGNIZED_KEYS = new HashSet<>(Arrays.asList("validator", "validationLevel", "validationAction"));

@Override
public List<TemplatePayloadValidationError> validate(MongoOperation operation) {
List<TemplatePayloadValidationError> errors = new ArrayList<>();
Map<String, Object> params = operation.getParameters();

if (params == null || !containsAnyRecognizedKey(params)) {
errors.add(new TemplatePayloadValidationError("parameters",
"ModifyCollection operation requires 'parameters' with at least one of: 'validator', 'validationLevel', 'validationAction'"));
return errors;
}

Object validator = params.get("validator");
if (validator != null && !(validator instanceof Map)) {
errors.add(new TemplatePayloadValidationError("parameters.validator",
"'validator' must be a document (map)"));
}

Object validationLevel = params.get("validationLevel");
if (validationLevel != null && (!(validationLevel instanceof String) || !VALID_VALIDATION_LEVELS.contains((String) validationLevel))) {
errors.add(new TemplatePayloadValidationError("parameters.validationLevel",
"'validationLevel' must be one of: 'off', 'strict', 'moderate'"));
}

Object validationAction = params.get("validationAction");
if (validationAction != null && (!(validationAction instanceof String) || !VALID_VALIDATION_ACTIONS.contains((String) validationAction))) {
errors.add(new TemplatePayloadValidationError("parameters.validationAction",
"'validationAction' must be one of: 'error', 'warn'"));
}

return errors;
}

private boolean containsAnyRecognizedKey(Map<String, Object> params) {
for (String key : RECOGNIZED_KEYS) {
if (params.containsKey(key)) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -713,12 +713,173 @@ void dropViewValidTest() {
assertTrue(errors.isEmpty());
}

}

@Nested
@DisplayName("ModifyCollection Validation Tests")
class ModifyCollectionValidationTests {

@Test
@DisplayName("WHEN modifyCollection has null parameters THEN validation fails")
void modifyCollectionNullParametersTest() {
MongoOperation op = new MongoOperation();
op.setType("modifyCollection");
op.setCollection("test");
op.setParameters(null);

List<TemplatePayloadValidationError> errors = op.validate();

assertEquals(1, errors.size());
assertEquals("parameters", errors.get(0).getField());
assertTrue(errors.get(0).getMessage().contains("at least one of"));
}

@Test
@DisplayName("WHEN modifyCollection is valid THEN validation passes")
void modifyCollectionValidTest() {
@DisplayName("WHEN modifyCollection has empty parameters THEN validation fails")
void modifyCollectionEmptyParametersTest() {
MongoOperation op = new MongoOperation();
op.setType("modifyCollection");
op.setCollection("test");
op.setParameters(new HashMap<>());

List<TemplatePayloadValidationError> errors = op.validate();

assertEquals(1, errors.size());
assertEquals("parameters", errors.get(0).getField());
assertTrue(errors.get(0).getMessage().contains("at least one of"));
}

@Test
@DisplayName("WHEN modifyCollection has invalid validationLevel THEN validation fails")
void modifyCollectionInvalidValidationLevelTest() {
MongoOperation op = new MongoOperation();
op.setType("modifyCollection");
op.setCollection("test");
Map<String, Object> params = new HashMap<>();
params.put("validationLevel", "banana");
op.setParameters(params);

List<TemplatePayloadValidationError> errors = op.validate();

assertEquals(1, errors.size());
assertEquals("parameters.validationLevel", errors.get(0).getField());
assertTrue(errors.get(0).getMessage().contains("'off', 'strict', 'moderate'"));
}

@Test
@DisplayName("WHEN modifyCollection has invalid validationAction THEN validation fails")
void modifyCollectionInvalidValidationActionTest() {
MongoOperation op = new MongoOperation();
op.setType("modifyCollection");
op.setCollection("test");
Map<String, Object> params = new HashMap<>();
params.put("validationAction", "ignore");
op.setParameters(params);

List<TemplatePayloadValidationError> errors = op.validate();

assertEquals(1, errors.size());
assertEquals("parameters.validationAction", errors.get(0).getField());
assertTrue(errors.get(0).getMessage().contains("'error', 'warn'"));
}

@Test
@DisplayName("WHEN modifyCollection has validator as wrong type THEN validation fails")
void modifyCollectionValidatorWrongTypeTest() {
MongoOperation op = new MongoOperation();
op.setType("modifyCollection");
op.setCollection("test");
Map<String, Object> params = new HashMap<>();
params.put("validator", "not a map");
op.setParameters(params);

List<TemplatePayloadValidationError> errors = op.validate();

assertEquals(1, errors.size());
assertEquals("parameters.validator", errors.get(0).getField());
assertTrue(errors.get(0).getMessage().contains("must be a document"));
}

@Test
@DisplayName("WHEN modifyCollection has multiple invalid params THEN multiple errors reported")
void modifyCollectionMultipleInvalidParamsTest() {
MongoOperation op = new MongoOperation();
op.setType("modifyCollection");
op.setCollection("test");
Map<String, Object> params = new HashMap<>();
params.put("validationLevel", "banana");
params.put("validationAction", "ignore");
op.setParameters(params);

List<TemplatePayloadValidationError> errors = op.validate();

assertEquals(2, errors.size());
}

@Test
@DisplayName("WHEN modifyCollection has only valid validator THEN validation passes")
void modifyCollectionValidValidatorOnlyTest() {
MongoOperation op = new MongoOperation();
op.setType("modifyCollection");
op.setCollection("test");
Map<String, Object> params = new HashMap<>();
Map<String, Object> validator = new HashMap<>();
validator.put("$jsonSchema", new HashMap<>());
params.put("validator", validator);
op.setParameters(params);

List<TemplatePayloadValidationError> errors = op.validate();

assertTrue(errors.isEmpty());
}

@Test
@DisplayName("WHEN modifyCollection has only valid validationLevel THEN validation passes")
void modifyCollectionValidValidationLevelOnlyTest() {
for (String level : Arrays.asList("off", "strict", "moderate")) {
MongoOperation op = new MongoOperation();
op.setType("modifyCollection");
op.setCollection("test");
Map<String, Object> params = new HashMap<>();
params.put("validationLevel", level);
op.setParameters(params);

List<TemplatePayloadValidationError> errors = op.validate();

assertTrue(errors.isEmpty(), "Expected no errors for validationLevel='" + level + "'");
}
}

@Test
@DisplayName("WHEN modifyCollection has only valid validationAction THEN validation passes")
void modifyCollectionValidValidationActionOnlyTest() {
for (String action : Arrays.asList("error", "warn")) {
MongoOperation op = new MongoOperation();
op.setType("modifyCollection");
op.setCollection("test");
Map<String, Object> params = new HashMap<>();
params.put("validationAction", action);
op.setParameters(params);

List<TemplatePayloadValidationError> errors = op.validate();

assertTrue(errors.isEmpty(), "Expected no errors for validationAction='" + action + "'");
}
}

@Test
@DisplayName("WHEN modifyCollection has all three valid params THEN validation passes")
void modifyCollectionAllValidParamsTest() {
MongoOperation op = new MongoOperation();
op.setType("modifyCollection");
op.setCollection("test");
Map<String, Object> params = new HashMap<>();
Map<String, Object> validator = new HashMap<>();
validator.put("$jsonSchema", new HashMap<>());
params.put("validator", validator);
params.put("validationLevel", "strict");
params.put("validationAction", "error");
op.setParameters(params);

List<TemplatePayloadValidationError> errors = op.validate();

Expand Down
Loading