diff --git a/ANALYSIS.md b/ANALYSIS.md index ab46777..8ae81d6 100644 --- a/ANALYSIS.md +++ b/ANALYSIS.md @@ -25,7 +25,7 @@ Structural validation runs at **load time** — before any change executes — v - `MongoOperationType` — Enum with 11 operations + factory + validator binding - `CollectionValidator` — Validates collection name (null, empty, `$`, `\0`) - `OperationValidator` — Interface for per-operation parameter validators + unrecognized key utility -- 8 parameter validators (`InsertParametersValidator`, `UpdateParametersValidator`, `DeleteParametersValidator`, `CreateIndexParametersValidator`, `DropIndexParametersValidator`, `RenameCollectionParametersValidator`, `CreateViewParametersValidator`, `ModifyCollectionParametersValidator`) +- 8 parameter validators (`InsertParametersValidator`, `UpdateParametersValidator`, `DeleteParametersValidator`, `CreateIndexParametersValidator`, `DropIndexParametersValidator`, `RenameCollectionParametersValidator`, `CreateViewParametersValidator`, `ModifyCollectionParametersValidator`) + `NoParametersValidator` for operations that don't accept parameters - 11 operator classes (`CreateCollectionOperator`, `InsertOperator`, etc.) - 7 mapper classes (`IndexOptionsMapper`, `InsertOptionsMapper`, `UpdateOptionsMapper`, `CreateViewOptionsMapper`, `RenameCollectionOptionsMapper`, `MapperUtil`, `BsonConverter`) @@ -81,8 +81,8 @@ All 10 issues identified in the original analysis have been resolved through mul | Operation | Enum Value | Operator Class | Transactional | Validation | Options Mapper | Session Handling | Operator Test | Integration Test | |------------------|---------------------|----------------------------|:-------------:|:---------------------------------------------------:|:-------------------------------:|:-------------------------:|:----------------------------------:|:------------------------------:| -| createCollection | `CREATE_COLLECTION` | `CreateCollectionOperator` | No | collection only | None | Ignores (base class logs) | `CreateCollectionOperatorTest` (2) | YAML `_0001` | -| dropCollection | `DROP_COLLECTION` | `DropCollectionOperator` | No | collection only | None | Ignores (base class logs) | `DropCollectionOperatorTest` (2) | YAML `_0004` rollback | +| createCollection | `CREATE_COLLECTION` | `CreateCollectionOperator` | No | NoParametersValidator | None | Ignores (base class logs) | `CreateCollectionOperatorTest` (2) | 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 | @@ -91,14 +91,15 @@ All 10 issues identified in the original analysis have been resolved through mul | renameCollection | `RENAME_COLLECTION` | `RenameCollectionOperator` | No | Full (target, options) | `RenameCollectionOptionsMapper` | Ignores (base class logs) | `RenameCollectionOperatorTest` (2) | None | | modifyCollection | `MODIFY_COLLECTION` | `ModifyCollectionOperator` | No | Full (validator, validationLevel, validationAction) | None | Ignores (base class logs) | `ModifyCollectionOperatorTest` (2) | None | | createView | `CREATE_VIEW` | `CreateViewOperator` | No | Full (viewOn, pipeline, options) | `CreateViewOptionsMapper` | Ignores (base class logs) | `CreateViewOperatorTest` (2) | None | -| dropView | `DROP_VIEW` | `DropViewOperator` | No | collection only | None | Ignores (base class logs) | `DropViewOperatorTest` (2) | 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 -- All 11 operations now have comprehensive load-time validation via `MongoOperation.validate()` +- 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,7 +110,7 @@ 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 | -| `MongoOperationValidateTest` | 81 | Unit (pure logic, nested classes) | No | +| `MongoOperationValidateTest` | 94 | Unit (pure logic, nested classes) | No | | `InsertOperatorTest` | 4 | Integration (operator-level) | Yes | | `UpdateOperatorTest` | 7 | Integration (operator-level) | Yes | | `DeleteOperatorTest` | 6 | Integration (operator-level) | Yes | @@ -128,9 +129,10 @@ All 10 issues identified in the original analysis have been resolved through mul | `UpdateOptionsMapperTest` | 8 | Unit | No | | `RenameCollectionOptionsMapperTest` | 4 | Unit | No | | `CreateViewOptionsMapperTest` | 3 | Unit | No | -| **Total** | **~207** | | | +| `MongoOperatorExceptionWrappingTest`| 5 | Unit | No | +| **Total** | **~225** | | | -Unit tests (no Docker): ~162 | Integration tests (Docker required): ~45 +Unit tests (no Docker): ~180 | Integration tests (Docker required): ~45 ### 4.2 Remaining Test Gaps @@ -164,7 +166,7 @@ No test verifies behavior when operations are re-applied (e.g., `createCollectio | Null input handling | GOOD | Validators catch null parameters at load time. `MongoOperation.validate()` handles null type, null collection, null parameters. Type checks prevent NPEs in getters | | Type safety | GOOD | Top-level parameters and nested elements are type-checked. Insert documents and createView pipeline stages are validated as Maps via `checkListElementTypes()`. Update `multi` is validated as Boolean. See section 5.1 (all resolved) | | Error collection (vs fail-fast) | GOOD | Individual validators collect all errors per operation. `MongoOperation.validate()` aggregates errors from `CollectionValidator` + `OperationValidator`. Framework collects across all payloads before any change runs | -| Exception hierarchy | GOOD | Uses the framework's `TemplatePayloadValidationError` with structured field/message pairs. No custom exception classes needed — the framework handles error presentation | +| Exception hierarchy | GOOD | Uses the framework's `TemplatePayloadValidationError` with structured field/message pairs for validation. `MongoTemplateExecutionException` wraps driver exceptions with operation type and collection context | | Logging | GOOD | `MongoOperator` base class logs transactional/non-transactional status for every operation. Clear INFO message when a non-transactional operation receives a session | | Immutability | WEAK | `MongoOperation` is fully mutable (public setters). No defensive copying of `parameters` map. Acceptable since instances are per-change and not shared | | Thread safety | N/A | Template instances are per-execution, not shared. Static logger is safe | @@ -221,21 +223,24 @@ No operator handles pre-existing state. This is critical for **retry scenarios** **Summary:** 4 operations need idempotency handling (`createCollection` is the most critical), 5 are already idempotent, 2 should remain non-idempotent (the failure is meaningful). -### 5.3 Silent Validation Gaps +### 5.3 Silent Validation Gaps — ALL RESOLVED -These are cases where **invalid or unexpected YAML is silently accepted** rather than flagged — the operation succeeds but doesn't do what the user intended. +All 3 gaps identified below have been resolved. Operations now reject unrecognized parameters and option keys at load time, and MongoDB driver exceptions are wrapped with template-level context. -#### #1 — `createCollection`, `dropCollection`, `dropView` accept unrecognized parameters silently +#### #1 — ~~`createCollection`, `dropCollection`, `dropView` accept unrecognized parameters silently~~ RESOLVED **Severity: MEDIUM** -These three operations use `OperationValidator.NO_OP`, so their `parameters` map is never inspected. YAML like `createCollection` with `parameters: { capped: true }` is silently ignored — the user thinks they created a capped collection but they didn't. Every other operation catches typos via `checkUnrecognizedKeys`, but these three don't. At minimum, they should warn when unexpected parameters are present. +**Original issue:** These three operations used `OperationValidator.NO_OP`, so their `parameters` map was never inspected. YAML like `createCollection` with `parameters: { capped: true }` was silently ignored. +**Resolution:** Replaced `OperationValidator.NO_OP` with `NoParametersValidator` for all three operations. Non-null, non-empty parameters now produce `"X operation does not accept parameters"`. Empty parameters (`{}`) and null are still accepted. The `NO_OP` constant was removed from `OperationValidator`. -#### #2 — Option mappers silently ignore unrecognized option keys +#### #2 — ~~Option mappers silently ignore unrecognized option keys~~ RESOLVED **Severity: MEDIUM** -All 5 option mappers (`IndexOptionsMapper`, `InsertOptionsMapper`, `UpdateOptionsMapper`, `CreateViewOptionsMapper`, `RenameCollectionOptionsMapper`) only process known keys and silently skip everything else. YAML like `options: { banana: true }` is discarded without any feedback. The user believes they configured an option but nothing happened. This is the same class of problem that `checkUnrecognizedKeys` solves for top-level parameters, but it's not applied inside `options`. +**Original issue:** All 5 option mappers only processed known keys and silently skipped everything else. YAML like `options: { banana: true }` was discarded without feedback. +**Resolution:** Added `RECOGNIZED_KEYS` constant to all 5 option mappers (`IndexOptionsMapper`: 20 keys, `InsertOptionsMapper`: 2, `UpdateOptionsMapper`: 4, `CreateViewOptionsMapper`: 1, `RenameCollectionOptionsMapper`: 1). Added `checkUnrecognizedOptionKeys()` utility to `OperationValidator`. All 5 parameter validators that handle `options` now validate option keys when the options map is valid, producing errors like `"X operation does not recognize option 'key'"`. -#### #3 — No operator wraps MongoDB driver exceptions with context +#### #3 — ~~No operator wraps MongoDB driver exceptions with context~~ RESOLVED **Severity: LOW** -When a MongoDB driver operation fails (e.g., `MongoCommandException`, `MongoBulkWriteException`), the raw exception bubbles up with no indication of which operation in a multi-step change failed or what YAML produced it. For a single-step change this is manageable, but for multi-step changes with 10+ operations, debugging requires matching stack traces to YAML manually. +**Original issue:** Raw MongoDB driver exceptions bubbled up with no indication of which operation in a multi-step change failed. +**Resolution:** `MongoOperator.apply()` now wraps `applyInternal()` exceptions in `MongoTemplateExecutionException`, which includes the operation type and collection name: `"Failed to execute '' on collection '': "`. Already-wrapped exceptions are re-thrown without double-wrapping. The original exception is preserved as the cause. --- @@ -331,25 +336,25 @@ Only `validator`, `validationLevel`, and `validationAction` are supported. Missi | Category | Weight | Score (1-10) | Weighted | |--------------------------------|:--------:|:------------:|:------------:| | Architecture & Design | 15% | 9 | 1.35 | -| Implementation Correctness | 15% | 7 | 1.05 | -| Validation & Error Handling | 20% | 7 | 1.40 | +| Implementation Correctness | 15% | 8 | 1.20 | +| Validation & Error Handling | 20% | 8 | 1.60 | | Template Feature Completeness | 15% | 6 | 0.90 | | Test Coverage | 20% | 6 | 1.20 | | Security & Safety | 10% | 8 | 0.80 | | Code Quality & Maintainability | 5% | 8 | 0.40 | -| **Total** | **100%** | | **7.1 / 10** | +| **Total** | **100%** | | **7.45 / 10** | ### Score Justification **Architecture (9/10):** Solid layered design with clean separation of concerns. The validation architecture was significantly improved by leveraging the framework's `TemplatePayload` contract — load-time validation is now built into the data model rather than being a separate step. Enum factory with validator binding is elegant. Template method pattern in operators is well-designed. -**Implementation Correctness (7/10):** All 10 original correctness issues have been resolved. Rollback validation is now handled by the framework. All operators have correct transactional flags. Collation mapping works for YAML input. Delete supports `deleteOne`/`deleteMany`. However, **4 of 11 operations are not idempotent** and will fail on retry in multi-step changes (section 5.2). `createCollection` is the most critical — it throws `MongoCommandException` if the collection already exists. No operator wraps MongoDB driver exceptions with context, making failures in multi-step changes hard to debug (section 5.3 #3). +**Implementation Correctness (8/10):** All 10 original correctness issues have been resolved. Rollback validation is now handled by the framework. All operators have correct transactional flags. Collation mapping works for YAML input. Delete supports `deleteOne`/`deleteMany`. MongoDB driver exceptions are now wrapped with template-level context (`MongoTemplateExecutionException`) for easier debugging of multi-step changes. However, **4 of 11 operations are not idempotent** and will fail on retry in multi-step changes (section 5.2). `createCollection` is the most critical — it throws `MongoCommandException` if the collection already exists. -**Validation (7/10):** Good load-time validation architecture with 8 dedicated parameter validators + `CollectionValidator`. Top-level types are checked, unrecognized keys are rejected, nested element types are validated (insert documents, createView pipeline stages checked as Maps; update `multi` checked as Boolean), and multiple errors are collected. All 5 validation-to-execution gaps (section 5.1) have been resolved — `ClassCastException` paths are eliminated. Two remaining gaps reduce this score: (1) **3 operations accept unrecognized parameters silently** — `createCollection`, `dropCollection`, `dropView` use `NO_OP` validator (section 5.3 #1); (2) **option mappers silently ignore unrecognized keys** — `options: { banana: true }` is discarded without feedback (section 5.3 #2). +**Validation (8/10):** Comprehensive load-time validation architecture with 8 dedicated parameter validators + `NoParametersValidator` + `CollectionValidator`. Top-level types are checked, unrecognized parameter keys are rejected, nested element types are validated (insert documents, createView pipeline stages checked as Maps; update `multi` checked as Boolean), and multiple errors are collected. All 5 validation-to-execution gaps (section 5.1) and all 3 silent validation gaps (section 5.3) have been resolved. `NoParametersValidator` now catches unexpected parameters on `createCollection`/`dropCollection`/`dropView`. Unrecognized option keys inside `options` are detected via `checkUnrecognizedOptionKeys` with `RECOGNIZED_KEYS` sets in all 5 mappers. The remaining deduction is for edge cases in option value validation (e.g., `unique: "yes"` instead of `true` is not caught at load time — only at mapper invocation). **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 (6/10):** Dramatically expanded from the original analysis. 81 validation tests cover all 11 operations with type checks, missing parameters, unrecognized keys, nested element type checks, and error accumulation. 82 mapper unit tests cover all option conversions including collation. Operator tests expanded (UpdateOperatorTest: 7, DeleteOperatorTest: 6). However: zero transactional path tests, zero options-with-operator integration tests, and zero idempotency tests. ~207 total tests. +**Test Coverage (6/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). However: zero transactional path tests, zero options-with-operator integration tests, and zero idempotency tests. ~225 total tests. **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. @@ -357,7 +362,7 @@ Only `validator`, `validationLevel`, and `validationAction` are supported. Missi ### Bottom Line -The module has a **solid architecture and clean codebase**, but gaps remain in robustness and feature completeness. All 10 original correctness issues and all 5 validation-to-execution gaps have been resolved. However, two areas need attention before the template is fully production-ready: (1) **Idempotency** — 4 DDL operations fail on retry instead of skipping, making multi-step changes fragile (section 5.2); (2) **Feature gaps** — missing `replaceOne`, inconsistent options support, bare-bones `createCollection` (section 8). Minor validation gaps remain: 3 operations accept unrecognized parameters silently and option mappers ignore unknown keys (section 5.3). The module is **functional for simple and moderately complex changes** but needs idempotency hardening for robust multi-step scenarios. +The module has a **solid architecture and clean codebase**, with comprehensive validation and error handling. All 10 original correctness issues, all 5 validation-to-execution gaps, and all 3 silent validation gaps have been resolved. MongoDB driver exceptions are now wrapped with template-level context for easier debugging. Two areas remain for future improvement: (1) **Idempotency** — 4 DDL operations fail on retry instead of skipping, making multi-step changes fragile (section 5.2); (2) **Feature gaps** — missing `replaceOne`, inconsistent options support, bare-bones `createCollection` (section 8). The module is **functional for simple and moderately complex changes** but needs idempotency hardening for robust multi-step scenarios. --- diff --git a/src/main/java/io/flamingock/template/mongodb/MongoTemplateExecutionException.java b/src/main/java/io/flamingock/template/mongodb/MongoTemplateExecutionException.java new file mode 100644 index 0000000..882784c --- /dev/null +++ b/src/main/java/io/flamingock/template/mongodb/MongoTemplateExecutionException.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * Wraps MongoDB driver exceptions with template-level context (operation type and collection name), + * making failures in multi-step changes easier to debug. The original exception is preserved as the cause. + */ +public class MongoTemplateExecutionException extends RuntimeException { + + public MongoTemplateExecutionException(String type, String collection, Throwable cause) { + super("Failed to execute '" + type + "' on collection '" + collection + "': " + cause.getMessage(), cause); + } +} diff --git a/src/main/java/io/flamingock/template/mongodb/mapper/CreateViewOptionsMapper.java b/src/main/java/io/flamingock/template/mongodb/mapper/CreateViewOptionsMapper.java index a528bbe..bb7fc53 100644 --- a/src/main/java/io/flamingock/template/mongodb/mapper/CreateViewOptionsMapper.java +++ b/src/main/java/io/flamingock/template/mongodb/mapper/CreateViewOptionsMapper.java @@ -17,12 +17,16 @@ import com.mongodb.client.model.CreateViewOptions; +import java.util.Collections; import java.util.Map; +import java.util.Set; import static io.flamingock.template.mongodb.mapper.MapperUtil.getCollation; public final class CreateViewOptionsMapper { + public static final Set RECOGNIZED_KEYS = Collections.singleton("collation"); + private CreateViewOptionsMapper() {} public static CreateViewOptions map(Map options) { diff --git a/src/main/java/io/flamingock/template/mongodb/mapper/IndexOptionsMapper.java b/src/main/java/io/flamingock/template/mongodb/mapper/IndexOptionsMapper.java index c9e28b5..bbdae97 100644 --- a/src/main/java/io/flamingock/template/mongodb/mapper/IndexOptionsMapper.java +++ b/src/main/java/io/flamingock/template/mongodb/mapper/IndexOptionsMapper.java @@ -17,7 +17,11 @@ import com.mongodb.client.model.IndexOptions; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import static io.flamingock.template.mongodb.mapper.MapperUtil.getBoolean; import static io.flamingock.template.mongodb.mapper.MapperUtil.getBson; @@ -32,6 +36,14 @@ public final class IndexOptionsMapper { + public static final Set RECOGNIZED_KEYS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "background", "unique", "name", "sparse", "expireAfterSeconds", + "version", "weights", "defaultLanguage", "languageOverride", + "textVersion", "sphereVersion", "bits", "min", "max", + "bucketSize", "storageEngine", "partialFilterExpression", + "collation", "wildcardProjection", "hidden" + ))); + private IndexOptionsMapper() {} public static IndexOptions mapToIndexOptions(Map options) { diff --git a/src/main/java/io/flamingock/template/mongodb/mapper/InsertOptionsMapper.java b/src/main/java/io/flamingock/template/mongodb/mapper/InsertOptionsMapper.java index 96a2f56..bf6c05f 100644 --- a/src/main/java/io/flamingock/template/mongodb/mapper/InsertOptionsMapper.java +++ b/src/main/java/io/flamingock/template/mongodb/mapper/InsertOptionsMapper.java @@ -18,12 +18,20 @@ import com.mongodb.client.model.InsertManyOptions; import com.mongodb.client.model.InsertOneOptions; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import static io.flamingock.template.mongodb.mapper.MapperUtil.getBoolean; public final class InsertOptionsMapper { + public static final Set RECOGNIZED_KEYS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "bypassDocumentValidation", "ordered" + ))); + private InsertOptionsMapper() {} public static InsertOneOptions mapToInsertOneOptions(Map options) { diff --git a/src/main/java/io/flamingock/template/mongodb/mapper/RenameCollectionOptionsMapper.java b/src/main/java/io/flamingock/template/mongodb/mapper/RenameCollectionOptionsMapper.java index ade56d8..8c5d797 100644 --- a/src/main/java/io/flamingock/template/mongodb/mapper/RenameCollectionOptionsMapper.java +++ b/src/main/java/io/flamingock/template/mongodb/mapper/RenameCollectionOptionsMapper.java @@ -17,12 +17,16 @@ import com.mongodb.client.model.RenameCollectionOptions; +import java.util.Collections; import java.util.Map; +import java.util.Set; import static io.flamingock.template.mongodb.mapper.MapperUtil.getBoolean; public final class RenameCollectionOptionsMapper { + public static final Set RECOGNIZED_KEYS = Collections.singleton("dropTarget"); + private RenameCollectionOptionsMapper() {} public static RenameCollectionOptions map(Map options) { diff --git a/src/main/java/io/flamingock/template/mongodb/mapper/UpdateOptionsMapper.java b/src/main/java/io/flamingock/template/mongodb/mapper/UpdateOptionsMapper.java index 90bf11a..9e6ae4d 100644 --- a/src/main/java/io/flamingock/template/mongodb/mapper/UpdateOptionsMapper.java +++ b/src/main/java/io/flamingock/template/mongodb/mapper/UpdateOptionsMapper.java @@ -17,13 +17,21 @@ import com.mongodb.client.model.UpdateOptions; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import static io.flamingock.template.mongodb.mapper.MapperUtil.getBoolean; import static io.flamingock.template.mongodb.mapper.MapperUtil.getCollation; public final class UpdateOptionsMapper { + public static final Set RECOGNIZED_KEYS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "upsert", "bypassDocumentValidation", "collation", "arrayFilters" + ))); + private UpdateOptionsMapper() {} public static UpdateOptions mapToUpdateOptions(Map options) { 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 2b664d0..9e006e6 100644 --- a/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java +++ b/src/main/java/io/flamingock/template/mongodb/model/MongoOperationType.java @@ -34,6 +34,7 @@ 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.NoParametersValidator; import io.flamingock.template.mongodb.validation.OperationValidator; import io.flamingock.template.mongodb.validation.RenameCollectionParametersValidator; import io.flamingock.template.mongodb.validation.UpdateParametersValidator; @@ -44,17 +45,17 @@ public enum MongoOperationType { - CREATE_COLLECTION("createCollection", CreateCollectionOperator::new, OperationValidator.NO_OP), + 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, OperationValidator.NO_OP), + 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, OperationValidator.NO_OP); + DROP_VIEW("dropView", DropViewOperator::new, new NoParametersValidator("DropView")); private final String value; private final BiFunction createOperatorFunction; diff --git a/src/main/java/io/flamingock/template/mongodb/model/operator/MongoOperator.java b/src/main/java/io/flamingock/template/mongodb/model/operator/MongoOperator.java index 5a738b6..e718490 100644 --- a/src/main/java/io/flamingock/template/mongodb/model/operator/MongoOperator.java +++ b/src/main/java/io/flamingock/template/mongodb/model/operator/MongoOperator.java @@ -17,6 +17,7 @@ import com.mongodb.client.ClientSession; import com.mongodb.client.MongoDatabase; +import io.flamingock.template.mongodb.MongoTemplateExecutionException; import io.flamingock.template.mongodb.model.MongoOperation; import io.flamingock.internal.util.log.FlamingockLoggerFactory; import org.slf4j.Logger; @@ -36,7 +37,13 @@ protected MongoOperator(MongoDatabase mongoDatabase, MongoOperation op, boolean public final void apply(ClientSession clientSession) { logOperation(clientSession != null); - applyInternal(clientSession); + try { + applyInternal(clientSession); + } catch (MongoTemplateExecutionException e) { + throw e; + } catch (Exception e) { + throw new MongoTemplateExecutionException(op.getType(), op.getCollection(), e); + } } private void logOperation(boolean withClientSession) { diff --git a/src/main/java/io/flamingock/template/mongodb/validation/CreateIndexParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/CreateIndexParametersValidator.java index 4d0c99a..84b6fd8 100644 --- a/src/main/java/io/flamingock/template/mongodb/validation/CreateIndexParametersValidator.java +++ b/src/main/java/io/flamingock/template/mongodb/validation/CreateIndexParametersValidator.java @@ -16,6 +16,7 @@ package io.flamingock.template.mongodb.validation; import io.flamingock.api.template.TemplatePayloadValidationError; +import io.flamingock.template.mongodb.mapper.IndexOptionsMapper; import io.flamingock.template.mongodb.model.MongoOperation; import java.util.ArrayList; @@ -62,6 +63,11 @@ public List validate(MongoOperation operation) { if (options != null && !(options instanceof Map)) { errors.add(new TemplatePayloadValidationError("parameters.options", "'options' must be a document")); + } else if (options instanceof Map) { + @SuppressWarnings("unchecked") + Map optionsMap = (Map) options; + errors.addAll(OperationValidator.checkUnrecognizedOptionKeys( + optionsMap, IndexOptionsMapper.RECOGNIZED_KEYS, "CreateIndex")); } errors.addAll(OperationValidator.checkUnrecognizedKeys(params, RECOGNIZED_KEYS, "CreateIndex")); diff --git a/src/main/java/io/flamingock/template/mongodb/validation/CreateViewParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/CreateViewParametersValidator.java index 358e2a3..8edac9a 100644 --- a/src/main/java/io/flamingock/template/mongodb/validation/CreateViewParametersValidator.java +++ b/src/main/java/io/flamingock/template/mongodb/validation/CreateViewParametersValidator.java @@ -16,6 +16,7 @@ package io.flamingock.template.mongodb.validation; import io.flamingock.api.template.TemplatePayloadValidationError; +import io.flamingock.template.mongodb.mapper.CreateViewOptionsMapper; import io.flamingock.template.mongodb.model.MongoOperation; import java.util.ArrayList; @@ -79,6 +80,11 @@ public List validate(MongoOperation operation) { if (options != null && !(options instanceof Map)) { errors.add(new TemplatePayloadValidationError("parameters.options", "'options' must be a document")); + } else if (options instanceof Map) { + @SuppressWarnings("unchecked") + Map optionsMap = (Map) options; + errors.addAll(OperationValidator.checkUnrecognizedOptionKeys( + optionsMap, CreateViewOptionsMapper.RECOGNIZED_KEYS, "CreateView")); } errors.addAll(OperationValidator.checkUnrecognizedKeys(params, RECOGNIZED_KEYS, "CreateView")); diff --git a/src/main/java/io/flamingock/template/mongodb/validation/InsertParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/InsertParametersValidator.java index e00a50a..019b674 100644 --- a/src/main/java/io/flamingock/template/mongodb/validation/InsertParametersValidator.java +++ b/src/main/java/io/flamingock/template/mongodb/validation/InsertParametersValidator.java @@ -16,6 +16,7 @@ package io.flamingock.template.mongodb.validation; import io.flamingock.api.template.TemplatePayloadValidationError; +import io.flamingock.template.mongodb.mapper.InsertOptionsMapper; import io.flamingock.template.mongodb.model.MongoOperation; import java.util.ArrayList; @@ -65,6 +66,11 @@ public List validate(MongoOperation operation) { if (options != null && !(options instanceof Map)) { errors.add(new TemplatePayloadValidationError("parameters.options", "'options' must be a document")); + } else if (options instanceof Map) { + @SuppressWarnings("unchecked") + Map optionsMap = (Map) options; + errors.addAll(OperationValidator.checkUnrecognizedOptionKeys( + optionsMap, InsertOptionsMapper.RECOGNIZED_KEYS, "Insert")); } errors.addAll(OperationValidator.checkUnrecognizedKeys(params, RECOGNIZED_KEYS, "Insert")); diff --git a/src/main/java/io/flamingock/template/mongodb/validation/NoParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/NoParametersValidator.java new file mode 100644 index 0000000..c137bab --- /dev/null +++ b/src/main/java/io/flamingock/template/mongodb/validation/NoParametersValidator.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.validation; + +import io.flamingock.api.template.TemplatePayloadValidationError; +import io.flamingock.template.mongodb.model.MongoOperation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Validator for operations that do not accept any parameters (e.g., createCollection, dropCollection, dropView). + * Rejects non-null, non-empty parameters to prevent silent misconfiguration. + */ +public class NoParametersValidator implements OperationValidator { + + private final String operationName; + + public NoParametersValidator(String operationName) { + this.operationName = operationName; + } + + @Override + public List validate(MongoOperation operation) { + Map params = operation.getParameters(); + if (params == null || params.isEmpty()) { + return Collections.emptyList(); + } + List errors = new ArrayList<>(); + errors.add(new TemplatePayloadValidationError("parameters", + operationName + " operation does not accept parameters")); + return errors; + } +} diff --git a/src/main/java/io/flamingock/template/mongodb/validation/OperationValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/OperationValidator.java index cd8009c..5377f24 100644 --- a/src/main/java/io/flamingock/template/mongodb/validation/OperationValidator.java +++ b/src/main/java/io/flamingock/template/mongodb/validation/OperationValidator.java @@ -19,7 +19,6 @@ import io.flamingock.template.mongodb.model.MongoOperation; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -27,8 +26,6 @@ @FunctionalInterface public interface OperationValidator { - OperationValidator NO_OP = operation -> Collections.emptyList(); - List validate(MongoOperation operation); static List checkUnrecognizedKeys( @@ -46,6 +43,21 @@ static List checkUnrecognizedKeys( return errors; } + static List checkUnrecognizedOptionKeys( + Map options, Set recognizedKeys, String operationName) { + List errors = new ArrayList<>(); + if (options == null) { + return errors; + } + for (String key : options.keySet()) { + if (!recognizedKeys.contains(key)) { + errors.add(new TemplatePayloadValidationError("parameters.options." + key, + operationName + " operation does not recognize option '" + key + "'")); + } + } + return errors; + } + static List checkListElementTypes( List list, String fieldPath, String elementName) { List errors = new ArrayList<>(); diff --git a/src/main/java/io/flamingock/template/mongodb/validation/RenameCollectionParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/RenameCollectionParametersValidator.java index e29ba06..3dc176b 100644 --- a/src/main/java/io/flamingock/template/mongodb/validation/RenameCollectionParametersValidator.java +++ b/src/main/java/io/flamingock/template/mongodb/validation/RenameCollectionParametersValidator.java @@ -16,6 +16,7 @@ package io.flamingock.template.mongodb.validation; import io.flamingock.api.template.TemplatePayloadValidationError; +import io.flamingock.template.mongodb.mapper.RenameCollectionOptionsMapper; import io.flamingock.template.mongodb.model.MongoOperation; import java.util.ArrayList; @@ -60,6 +61,11 @@ public List validate(MongoOperation operation) { if (options != null && !(options instanceof Map)) { errors.add(new TemplatePayloadValidationError("parameters.options", "'options' must be a document")); + } else if (options instanceof Map) { + @SuppressWarnings("unchecked") + Map optionsMap = (Map) options; + errors.addAll(OperationValidator.checkUnrecognizedOptionKeys( + optionsMap, RenameCollectionOptionsMapper.RECOGNIZED_KEYS, "RenameCollection")); } errors.addAll(OperationValidator.checkUnrecognizedKeys(params, RECOGNIZED_KEYS, "RenameCollection")); diff --git a/src/main/java/io/flamingock/template/mongodb/validation/UpdateParametersValidator.java b/src/main/java/io/flamingock/template/mongodb/validation/UpdateParametersValidator.java index 6a97f3e..966768c 100644 --- a/src/main/java/io/flamingock/template/mongodb/validation/UpdateParametersValidator.java +++ b/src/main/java/io/flamingock/template/mongodb/validation/UpdateParametersValidator.java @@ -16,6 +16,7 @@ package io.flamingock.template.mongodb.validation; import io.flamingock.api.template.TemplatePayloadValidationError; +import io.flamingock.template.mongodb.mapper.UpdateOptionsMapper; import io.flamingock.template.mongodb.model.MongoOperation; import java.util.ArrayList; @@ -68,6 +69,11 @@ public List validate(MongoOperation operation) { if (options != null && !(options instanceof Map)) { errors.add(new TemplatePayloadValidationError("parameters.options", "'options' must be a document")); + } else if (options instanceof Map) { + @SuppressWarnings("unchecked") + Map optionsMap = (Map) options; + errors.addAll(OperationValidator.checkUnrecognizedOptionKeys( + optionsMap, UpdateOptionsMapper.RECOGNIZED_KEYS, "Update")); } errors.addAll(OperationValidator.checkUnrecognizedKeys(params, RECOGNIZED_KEYS, "Update")); 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 da71554..a0f3cc4 100644 --- a/src/test/java/io/flamingock/template/mongodb/model/MongoOperationValidateTest.java +++ b/src/test/java/io/flamingock/template/mongodb/model/MongoOperationValidateTest.java @@ -1087,6 +1087,95 @@ void dropViewValidTest() { assertTrue(errors.isEmpty()); } + @Test + @DisplayName("WHEN createCollection has parameters THEN validation fails") + void createCollectionWithParametersTest() { + MongoOperation op = new MongoOperation(); + op.setType("createCollection"); + op.setCollection("test"); + Map params = new HashMap<>(); + params.put("capped", true); + op.setParameters(params); + + List errors = op.validate(); + + assertEquals(1, errors.size()); + assertEquals("parameters", errors.get(0).getField()); + assertTrue(errors.get(0).getMessage().contains("does not accept parameters")); + } + + @Test + @DisplayName("WHEN createCollection has empty parameters THEN validation passes") + void createCollectionEmptyParametersTest() { + MongoOperation op = new MongoOperation(); + op.setType("createCollection"); + op.setCollection("test"); + op.setParameters(new HashMap<>()); + + List errors = op.validate(); + + assertTrue(errors.isEmpty()); + } + + @Test + @DisplayName("WHEN dropCollection has parameters THEN validation fails") + void dropCollectionWithParametersTest() { + MongoOperation op = new MongoOperation(); + op.setType("dropCollection"); + op.setCollection("test"); + Map params = new HashMap<>(); + params.put("writeConcern", "majority"); + op.setParameters(params); + + List errors = op.validate(); + + assertEquals(1, errors.size()); + assertEquals("parameters", errors.get(0).getField()); + assertTrue(errors.get(0).getMessage().contains("does not accept parameters")); + } + + @Test + @DisplayName("WHEN dropCollection has empty parameters THEN validation passes") + void dropCollectionEmptyParametersTest() { + MongoOperation op = new MongoOperation(); + op.setType("dropCollection"); + op.setCollection("test"); + op.setParameters(new HashMap<>()); + + List errors = op.validate(); + + assertTrue(errors.isEmpty()); + } + + @Test + @DisplayName("WHEN dropView has parameters THEN validation fails") + void dropViewWithParametersTest() { + MongoOperation op = new MongoOperation(); + op.setType("dropView"); + op.setCollection("testView"); + Map params = new HashMap<>(); + params.put("force", true); + op.setParameters(params); + + List errors = op.validate(); + + assertEquals(1, errors.size()); + assertEquals("parameters", errors.get(0).getField()); + assertTrue(errors.get(0).getMessage().contains("does not accept parameters")); + } + + @Test + @DisplayName("WHEN dropView has empty parameters THEN validation passes") + void dropViewEmptyParametersTest() { + MongoOperation op = new MongoOperation(); + op.setType("dropView"); + op.setCollection("testView"); + op.setParameters(new HashMap<>()); + + List errors = op.validate(); + + assertTrue(errors.isEmpty()); + } } @Nested @@ -1439,4 +1528,163 @@ void modifyCollectionUnrecognizedKeyTest() { assertTrue(errors.get(0).getMessage().contains("does not recognize parameter 'unknownKey'")); } } + + @Nested + @DisplayName("Unrecognized Option Keys Tests") + class UnrecognizedOptionKeysTests { + + @Test + @DisplayName("WHEN insert has unrecognized option key THEN validation fails") + void insertUnrecognizedOptionKeyTest() { + MongoOperation op = new MongoOperation(); + op.setType("insert"); + op.setCollection("test"); + Map params = new HashMap<>(); + List> docs = new ArrayList<>(); + Map doc = new HashMap<>(); + doc.put("name", "Test"); + docs.add(doc); + params.put("documents", docs); + Map options = new HashMap<>(); + options.put("banana", true); + params.put("options", options); + op.setParameters(params); + + List errors = op.validate(); + + assertEquals(1, errors.size()); + assertEquals("parameters.options.banana", errors.get(0).getField()); + assertTrue(errors.get(0).getMessage().contains("does not recognize option 'banana'")); + } + + @Test + @DisplayName("WHEN update has unrecognized option key THEN validation fails") + void updateUnrecognizedOptionKeyTest() { + MongoOperation op = new MongoOperation(); + op.setType("update"); + op.setCollection("test"); + Map params = new HashMap<>(); + params.put("filter", new HashMap<>()); + Map update = new HashMap<>(); + update.put("$set", new HashMap<>()); + params.put("update", update); + Map options = new HashMap<>(); + options.put("unknownOption", "value"); + params.put("options", options); + op.setParameters(params); + + List errors = op.validate(); + + assertEquals(1, errors.size()); + assertEquals("parameters.options.unknownOption", errors.get(0).getField()); + assertTrue(errors.get(0).getMessage().contains("does not recognize option 'unknownOption'")); + } + + @Test + @DisplayName("WHEN createIndex has unrecognized option key THEN validation fails") + void createIndexUnrecognizedOptionKeyTest() { + MongoOperation op = new MongoOperation(); + op.setType("createIndex"); + op.setCollection("test"); + Map params = new HashMap<>(); + Map keys = new HashMap<>(); + keys.put("email", 1); + params.put("keys", keys); + Map options = new HashMap<>(); + options.put("invalidOption", true); + params.put("options", options); + op.setParameters(params); + + List errors = op.validate(); + + assertEquals(1, errors.size()); + assertEquals("parameters.options.invalidOption", errors.get(0).getField()); + assertTrue(errors.get(0).getMessage().contains("does not recognize option 'invalidOption'")); + } + + @Test + @DisplayName("WHEN createView has unrecognized option key THEN validation fails") + void createViewUnrecognizedOptionKeyTest() { + MongoOperation op = new MongoOperation(); + op.setType("createView"); + op.setCollection("testView"); + Map params = new HashMap<>(); + params.put("viewOn", "sourceCollection"); + params.put("pipeline", Collections.singletonList(new HashMap<>())); + Map options = new HashMap<>(); + options.put("unknownViewOption", "value"); + params.put("options", options); + op.setParameters(params); + + List errors = op.validate(); + + assertEquals(1, errors.size()); + assertEquals("parameters.options.unknownViewOption", errors.get(0).getField()); + assertTrue(errors.get(0).getMessage().contains("does not recognize option 'unknownViewOption'")); + } + + @Test + @DisplayName("WHEN renameCollection has unrecognized option key THEN validation fails") + void renameCollectionUnrecognizedOptionKeyTest() { + MongoOperation op = new MongoOperation(); + op.setType("renameCollection"); + op.setCollection("test"); + Map params = new HashMap<>(); + params.put("target", "newName"); + Map options = new HashMap<>(); + options.put("badOption", true); + params.put("options", options); + op.setParameters(params); + + List errors = op.validate(); + + assertEquals(1, errors.size()); + assertEquals("parameters.options.badOption", errors.get(0).getField()); + assertTrue(errors.get(0).getMessage().contains("does not recognize option 'badOption'")); + } + + @Test + @DisplayName("WHEN createIndex has recognized option keys THEN validation passes") + void createIndexRecognizedOptionKeyTest() { + MongoOperation op = new MongoOperation(); + op.setType("createIndex"); + op.setCollection("test"); + 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", "idx"); + params.put("options", options); + op.setParameters(params); + + List errors = op.validate(); + + assertTrue(errors.isEmpty()); + } + + @Test + @DisplayName("WHEN insert has multiple unrecognized option keys THEN all errors reported") + void insertMultipleUnrecognizedOptionKeysTest() { + MongoOperation op = new MongoOperation(); + op.setType("insert"); + op.setCollection("test"); + Map params = new HashMap<>(); + List> docs = new ArrayList<>(); + Map doc = new HashMap<>(); + doc.put("name", "Test"); + docs.add(doc); + params.put("documents", docs); + Map options = new HashMap<>(); + options.put("foo", true); + options.put("bar", false); + params.put("options", options); + op.setParameters(params); + + List errors = op.validate(); + + assertEquals(2, errors.size()); + } + } } diff --git a/src/test/java/io/flamingock/template/mongodb/model/operator/MongoOperatorExceptionWrappingTest.java b/src/test/java/io/flamingock/template/mongodb/model/operator/MongoOperatorExceptionWrappingTest.java new file mode 100644 index 0000000..70e5c18 --- /dev/null +++ b/src/test/java/io/flamingock/template/mongodb/model/operator/MongoOperatorExceptionWrappingTest.java @@ -0,0 +1,118 @@ +/* + * 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.model.operator; + +import com.mongodb.client.ClientSession; +import io.flamingock.template.mongodb.MongoTemplateExecutionException; +import io.flamingock.template.mongodb.model.MongoOperation; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MongoOperatorExceptionWrappingTest { + + private static MongoOperation createOp(String type, String collection) { + MongoOperation op = new MongoOperation(); + op.setType(type); + op.setCollection(collection); + return op; + } + + private static MongoOperator createOperator(MongoOperation op, Runnable action) { + return new MongoOperator(null, op, false) { + @Override + protected void applyInternal(ClientSession clientSession) { + if (action != null) { + action.run(); + } + } + }; + } + + @Test + @DisplayName("WHEN applyInternal throws RuntimeException THEN wrapped with MongoTemplateExecutionException") + void wrapsRuntimeExceptionTest() { + MongoOperation op = createOp("insert", "users"); + RuntimeException original = new RuntimeException("connection refused"); + MongoOperator operator = createOperator(op, () -> { throw original; }); + + MongoTemplateExecutionException ex = assertThrows( + MongoTemplateExecutionException.class, + () -> operator.apply(null) + ); + + assertTrue(ex.getMessage().contains("insert")); + assertTrue(ex.getMessage().contains("users")); + assertTrue(ex.getMessage().contains("connection refused")); + assertSame(original, ex.getCause()); + } + + @Test + @DisplayName("WHEN applyInternal throws checked-style Exception THEN wrapped with MongoTemplateExecutionException") + void wrapsMongoCommandExceptionTest() { + MongoOperation op = createOp("createIndex", "orders"); + IllegalStateException original = new IllegalStateException("duplicate key"); + MongoOperator operator = createOperator(op, () -> { throw original; }); + + MongoTemplateExecutionException ex = assertThrows( + MongoTemplateExecutionException.class, + () -> operator.apply(null) + ); + + assertTrue(ex.getMessage().contains("createIndex")); + assertTrue(ex.getMessage().contains("orders")); + assertSame(original, ex.getCause()); + } + + @Test + @DisplayName("WHEN applyInternal succeeds THEN no exception thrown") + void noExceptionOnSuccessTest() { + MongoOperation op = createOp("insert", "users"); + MongoOperator operator = createOperator(op, null); + + assertDoesNotThrow(() -> operator.apply(null)); + } + + @Test + @DisplayName("WHEN applyInternal throws MongoTemplateExecutionException THEN not double-wrapped") + void doesNotDoubleWrapTest() { + MongoOperation op = createOp("update", "products"); + MongoTemplateExecutionException original = + new MongoTemplateExecutionException("update", "products", new RuntimeException("inner")); + MongoOperator operator = createOperator(op, () -> { throw original; }); + + MongoTemplateExecutionException ex = assertThrows( + MongoTemplateExecutionException.class, + () -> operator.apply(null) + ); + + assertSame(original, ex); + } + + @Test + @DisplayName("WHEN exception constructed THEN message matches expected format") + void exceptionMessageFormatTest() { + MongoTemplateExecutionException ex = + new MongoTemplateExecutionException("delete", "logs", new RuntimeException("timeout")); + + assertEquals("Failed to execute 'delete' on collection 'logs': timeout", ex.getMessage()); + } +} diff --git a/src/test/java/io/flamingock/template/mongodb/operations/InsertOperatorTest.java b/src/test/java/io/flamingock/template/mongodb/operations/InsertOperatorTest.java index 260a7f4..588527a 100644 --- a/src/test/java/io/flamingock/template/mongodb/operations/InsertOperatorTest.java +++ b/src/test/java/io/flamingock/template/mongodb/operations/InsertOperatorTest.java @@ -15,6 +15,7 @@ */ 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; @@ -130,8 +131,11 @@ void insertEmptyDocumentsTest() { InsertOperator operator = new InsertOperator(mongoDatabase, operation); // Empty documents should be caught by InsertParametersValidator at load time. - // If the operator is called directly with empty documents, the MongoDB driver throws. - Assertions.assertThrows(IllegalArgumentException.class, () -> operator.apply(null)); + // If the operator is called directly with empty documents, the MongoDB driver throws + // IllegalArgumentException, which is wrapped by MongoOperator.apply(). + MongoTemplateExecutionException ex = Assertions.assertThrows( + MongoTemplateExecutionException.class, () -> operator.apply(null)); + Assertions.assertTrue(ex.getCause() instanceof IllegalArgumentException); } private long getDocumentCount() {