diff options
author | Mihai Andrei <mihai.andrei@10gen.com> | 2020-10-01 16:50:21 -0400 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2020-10-21 20:09:04 +0000 |
commit | 69c03caa427e1d31b81e7bc98a56d9eb7e6ab06b (patch) | |
tree | 0618b69d836eb2573f795120c6a0ce906311ac17 /src/mongo/db/matcher | |
parent | 306f04029f01d3bfb7bc565fb2139862daa57397 (diff) | |
download | mongo-69c03caa427e1d31b81e7bc98a56d9eb7e6ab06b.tar.gz |
SERVER-49446 Implement validation error generation for required keyword
Diffstat (limited to 'src/mongo/db/matcher')
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error.cpp | 224 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error.h | 4 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp | 769 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser.cpp | 28 |
4 files changed, 912 insertions, 113 deletions
diff --git a/src/mongo/db/matcher/doc_validation_error.cpp b/src/mongo/db/matcher/doc_validation_error.cpp index 913b6b0906a..68c8e88df6c 100644 --- a/src/mongo/db/matcher/doc_validation_error.cpp +++ b/src/mongo/db/matcher/doc_validation_error.cpp @@ -899,7 +899,8 @@ public: kNormalReason, kInvertedReason, &kExpectedTypes, - LeafArrayBehavior::kNoTraversal); + LeafArrayBehavior::kNoTraversal, + true /* isJsonSchemaKeyword */); } void visit(const InternalSchemaMatchArrayIndexMatchExpression* expr) final { _context->pushNewFrame(*expr); @@ -950,9 +951,11 @@ public: void visit(const InternalSchemaObjectMatchExpression* expr) final { // This node should never be responsible for generating an error directly. invariant(expr->getErrorAnnotation()->mode != AnnotationMode::kGenerateError); + // As part of pushing a new frame onto the stack, the runtime state may be set to // 'kNoError' if 'expr' matches the current document. _context->pushNewFrame(*expr); + // Only attempt to find a subdocument if this node failed to match. if (_context->getCurrentRuntimeState() != RuntimeState::kNoError) { ElementPath path(expr->path(), LeafArrayBehavior::kNoTraversal); @@ -984,12 +987,13 @@ public: } void visit(const InternalSchemaRootDocEqMatchExpression* expr) final {} void visit(const InternalSchemaTypeExpression* expr) final { - generateTypeError(expr, LeafArrayBehavior::kNoTraversal); + generateTypeError(*expr, LeafArrayBehavior::kNoTraversal, true /* isJsonSchemaKeyword */); } void visit(const InternalSchemaUniqueItemsMatchExpression* expr) final { static constexpr auto normalReason = "found a duplicate item"; _context->pushNewFrame(*expr); - if (auto attributeValue = getValueForArrayKeywordExpressionIfShouldGenerateError(*expr)) { + if (auto attributeValue = + getValueForKeywordExpressionIfShouldGenerateError(*expr, {BSONType::Array})) { appendErrorDetails(*expr); appendErrorReason(normalReason, ""); auto attributeValueAsArray = BSONArray(attributeValue.embeddedObject()); @@ -1070,7 +1074,8 @@ public: if (expr->getErrorAnnotation()->tag == "enum") { static constexpr auto kNormalReason = "value was not found in enum"; static constexpr auto kInvertedReason = "value was found in enum"; - generateLogicalLeafError(*expr, kNormalReason, kInvertedReason); + generateLogicalLeafError( + *expr, kNormalReason, kInvertedReason, true /* isJsonSchemaKeyword */); } else { preVisitTreeOperator(expr); // An OR needs its children to call 'matches' in an inverted context to discern which @@ -1085,7 +1090,13 @@ public: static constexpr auto kInvertedReason = "regular expression did match"; static const std::set<BSONType> kExpectedTypes{ BSONType::String, BSONType::Symbol, BSONType::RegEx}; - generatePathError(*expr, kNormalReason, kInvertedReason, &kExpectedTypes); + bool isJsonSchemaKeyword = expr->getErrorAnnotation()->tag == "pattern"; + generatePathError(*expr, + kNormalReason, + kInvertedReason, + &kExpectedTypes, + LeafArrayBehavior::kTraverseOmitArray, + isJsonSchemaKeyword); } void visit(const SizeMatchExpression* expr) final { static constexpr auto kNormalReason = "array length was not equal to given size"; @@ -1104,7 +1115,7 @@ public: // traversed array elements as considered values since, when we have predicate "{$type: // 'array'}" and a field is an array, that is a match. Therefore we use // LeafArrayBehavior::kTraverseOmitArray as the traversal behavior. - generateTypeError(expr, LeafArrayBehavior::kTraverseOmitArray); + generateTypeError(*expr, LeafArrayBehavior::kTraverseOmitArray); } void visit(const WhereMatchExpression* expr) final { MONGO_UNREACHABLE; @@ -1339,9 +1350,19 @@ private: const std::string& normalReason, const std::string& invertedReason, const std::set<BSONType>* expectedTypes = nullptr, - LeafArrayBehavior leafArrayBehavior = LeafArrayBehavior::kTraverseOmitArray) { + LeafArrayBehavior leafArrayBehavior = LeafArrayBehavior::kTraverseOmitArray, + bool isJsonSchemaKeyword = false) { _context->pushNewFrame(expr); if (_context->shouldGenerateError(expr)) { + // If this is a jsonSchema keyword, we must verify that expr's path exists and the + // value of the path matches the expected type. Otherwise, this node will not be + // responsible for an error; either the parent of expr will not match, or another + // node in the tree will generate an appropriate error. + if (isJsonSchemaKeyword && + !getValueForKeywordExpressionIfShouldGenerateError(expr, *expectedTypes)) { + _context->setCurrentRuntimeState(RuntimeState::kNoError); + return; + } appendErrorDetails(expr); auto arr = createValuesArray(expr.path(), leafArrayBehavior); appendMissingField(arr); @@ -1354,7 +1375,23 @@ private: void generateComparisonError(const ComparisonMatchExpression* expr) { static constexpr auto kNormalReason = "comparison failed"; static constexpr auto kInvertedReason = "comparison succeeded"; - generatePathError(*expr, kNormalReason, kInvertedReason); + // Determine whether 'expr' represents a jsonSchema minimum/maximum keyword. + static const std::set<std::string> kJsonSchemaKeywords = {"minimum", "maximum"}; + if (kJsonSchemaKeywords.find(expr->getErrorAnnotation()->tag) != + kJsonSchemaKeywords.end()) { + static const std::set<BSONType> kExpectedTypes{BSONType::NumberLong, + BSONType::NumberDouble, + BSONType::NumberDecimal, + BSONType::NumberInt}; + generatePathError(*expr, + kNormalReason, + kInvertedReason, + &kExpectedTypes, + LeafArrayBehavior::kNoTraversal, + true /* isJsonSchemaKeyword */); + } else { + generatePathError(*expr, kNormalReason, kInvertedReason); + } } void generateElemMatchError(const ArrayMatchingMatchExpression* expr) { @@ -1372,13 +1409,21 @@ private: } template <class T> - void generateTypeError(const TypeMatchExpressionBase<T>* expr, LeafArrayBehavior behavior) { - _context->pushNewFrame(*expr); + void generateTypeError(const TypeMatchExpressionBase<T>& expr, + LeafArrayBehavior behavior, + bool isJsonSchemaKeyword = false) { + _context->pushNewFrame(expr); static constexpr auto kNormalReason = "type did not match"; static constexpr auto kInvertedReason = "type did match"; - if (_context->shouldGenerateError(*expr)) { - appendErrorDetails(*expr); - auto arr = createValuesArray(expr->path(), behavior); + if (_context->shouldGenerateError(expr)) { + auto arr = createValuesArray(expr.path(), behavior); + // If the path of 'expr' is missing and this is a jsonSchema keyword, then this node + // should not generate an error. + if (isJsonSchemaKeyword && !arr) { + _context->setCurrentRuntimeState(RuntimeState::kNoError); + return; + } + appendErrorDetails(expr); appendMissingField(arr); appendErrorReason(kNormalReason, kInvertedReason); appendConsideredValues(arr); @@ -1408,11 +1453,18 @@ private: _context->pushNewFrame(*expr); if (_context->shouldGenerateError(*expr)) { auto annotation = expr->getErrorAnnotation(); + auto tag = annotation->tag; // Only append the operator name if it will produce an object error corresponding to // a user-facing operator. - if (!_context->producesArray(*expr)) + if (tag[0] != '_') appendOperatorName(*expr); - _context->getCurrentObjBuilder().appendElements(annotation->annotation); + auto& builder = _context->getCurrentObjBuilder(); + // Append the keyword specification when 'expr' corresponds to the 'required' keyword. + if (tag == "required") { + appendSpecifiedAs(*annotation, &builder); + } else { + _context->getCurrentObjBuilder().appendElements(annotation->annotation); + } } } /** @@ -1421,7 +1473,8 @@ private: */ void generateLogicalLeafError(const ListOfMatchExpression& expr, const std::string& normalReason, - const std::string& invertedReason) { + const std::string& invertedReason, + bool isJsonSchemaKeyword = false) { _context->pushNewFrame(expr); if (_context->shouldGenerateError(expr)) { // $all with no children should not translate to an 'AndMatchExpression' and 'enum' @@ -1430,6 +1483,13 @@ private: appendErrorDetails(expr); auto childExpr = expr.getChild(0); auto arr = createValuesArray(childExpr->path(), LeafArrayBehavior::kNoTraversal); + + // If this is a jsonSchema keyword and the value doesn't exist, then this node will + // not generate an error. + if (isJsonSchemaKeyword && !arr) { + _context->setCurrentRuntimeState(RuntimeState::kNoError); + return; + } appendMissingField(arr); appendErrorReason(normalReason, invertedReason); appendConsideredValues(arr); @@ -1463,19 +1523,23 @@ private: static constexpr auto kNormalReason = "specified string length was not satisfied"; static constexpr auto kInvertedReason = "specified string length was satisfied"; static const std::set<BSONType> expectedTypes{BSONType::String}; - generatePathError( - expr, kNormalReason, kInvertedReason, &expectedTypes, LeafArrayBehavior::kNoTraversal); + generatePathError(expr, + kNormalReason, + kInvertedReason, + &expectedTypes, + LeafArrayBehavior::kNoTraversal, + true /* isJsonSchemaKeyword */); } /** - * Determines if a validation error should be generated for a JSON Schema array keyword match - * expression 'expr' given the current document validation context and returns the array 'expr' - * expression applies over. If a validation error should not be generated, then the - * End-Of-Object (EOO) value is returned. If a validation error should be generated, then the - * type of the value of the returned BSONElement is always an array. + * Determines if a validation error should be generated for a JsonSchema keyword MatchExpression + * 'expr' given the current document validation context. Returns the element 'expr' applies + * over if the found element matches one of the 'expectedTypes'. By returning a non-empty + * element, this indicates that 'expr' should generate an error. Returns End-Of-Object (EOO) + * value otherwise, which indicates that 'expr' should not generate an error. */ - BSONElement getValueForArrayKeywordExpressionIfShouldGenerateError( - const MatchExpression& expr) { + BSONElement getValueForKeywordExpressionIfShouldGenerateError( + const MatchExpression& expr, const std::set<BSONType>& expectedTypes) { if (!_context->shouldGenerateError(expr)) { return {}; } @@ -1489,39 +1553,40 @@ private: expr.path(), LeafArrayBehavior::kNoTraversal, NonLeafArrayBehavior::kNoTraversal); auto attributeValue = getValueAt(path); - // If attribute value is either not present or is not an array, do not generate an error, - // since related match expressions do that instead. There are 4 cases of how an array - // keyword can be defined in combination with 'required' and 'type' keywords (in the - // explanation below parameter 'expr' corresponds to '(array keyword match expression)'): + // If attribute value is either not present or does not match the types in 'expectedTypes', + // do not generate an error, since related match expressions do that instead. There are 4 + // cases of how a keyword can be defined in combination with 'required' and 'type' keywords + // (in the explanation below parameter 'expr' corresponds to '(keyword match expression)'): // - // 1) 'required' is not present, {type: 'array'} is not present. In this case the expression - // tree corresponds to ((array keyword match expression) OR NOT (is array)) OR (NOT - // (attribute exists)). This tree can fail to match only if the attribute is present and is - // an array. + // 1) 'required' is not present, {type: <expectedTypes>} is not present. In this case the + // expression tree corresponds to ((keyword match expression) OR NOT (matches type)) OR + // (NOT (attribute exists)). This tree can fail to match only if the attribute is present + // and matches a type in 'expectedTypes'. // - // 2) 'required' is not present, {type: 'array'} is present. In this case the expression - // tree corresponds to ((array keyword match expression) AND (is array)) OR (NOT (attribute - // exists)). If the input is an attribute of a non-array type, then both (array keyword - // match expression) and (is array) expressions fail to match and are asked to contribute to - // the validation error. We expect only (is array) expression, not an (array keyword match - // expression), to report a type mismatch, since otherwise the error would contain redundant - // elements. + // 2) 'required' is not present, {type: <expectedTypes>} is present. In this case the + // expression tree corresponds to ((keyword match expression) AND (matches type)) OR (NOT + // (attribute exists)). If the input is an element of a non-matching type, then both + // (keyword match expression) and (matches type) expressions fail to match and are asked + // to contribute to the validation error. We expect only (matches type) expression, not a + // (keyword match expression), to report a type mismatch, since otherwise the error would + // contain redundant elements. // - // 3) 'required' is present, {type: 'array'} is not present. In this case the expression - // tree corresponds to ((array keyword match expression) OR NOT (is array)) AND (attribute - // exists). This tree can fail to match if the attribute is present and is an array, and - // fails to match when the attribute is not present. In the latter case expression part - // ((array keyword match expression) OR NOT (is array)) matches and (array keyword match - // expression) is not asked to contribute to the error. + // 3) 'required' is present, {type: <expectedTypes>} is not present. In this case the + // expression tree corresponds to ((keyword match expression) OR NOT (matches type)) AND + // (attribute exists). This tree can fail to match if the attribute is present and + // matches a type, and fails to match when the attribute is not present. In the latter + // case, the expression part ((keyword match expression) OR NOT (matches type)) matches and + // (keyword match expression) is not asked to contribute to the error. // - // 4) 'required' is present, {type: 'array'} is present. In this case the expression tree - // corresponds to ((array keyword match expression) AND (is array)) AND (attribute exists). - // This tree can fail to match if the attribute is present and is an array, and fails to - // match when the attribute is not present or is not an array. In the case when the + // 4) 'required' is present, {type: <expectedTypes>} is present. In this case the expression + // tree corresponds to ((keyword match expression) AND (matches type)) AND (attribute + // exists). This tree can fail to match if the attribute is present and matches a type, + // or if the attribute is not present or does not match a type. In the case when the // attribute is not present all parts of the expression fail to match and are asked to // contribute to the error, but we expect only (attribute exists) expression to contribute, - // since otherwise the error would contain redundant elements. - return (attributeValue.type() == BSONType::Array) ? attributeValue : BSONElement{}; + // since otherwise the error would contain redundant elements. + return expectedTypes.find(attributeValue.type()) != expectedTypes.end() ? attributeValue + : BSONElement{}; } /** @@ -1531,7 +1596,8 @@ private: const InternalSchemaNumArrayItemsMatchExpression* expr) { static constexpr auto normalReason = "array did not match specified length"; _context->pushNewFrame(*expr); - if (auto attributeValue = getValueForArrayKeywordExpressionIfShouldGenerateError(*expr)) { + if (auto attributeValue = + getValueForKeywordExpressionIfShouldGenerateError(*expr, {BSONType::Array})) { appendErrorDetails(*expr); appendErrorReason(normalReason, ""); auto attributeValueAsArray = BSONArray(attributeValue.embeddedObject()); @@ -1548,7 +1614,8 @@ private: const InternalSchemaAllElemMatchFromIndexMatchExpression* expr) { static constexpr auto normalReason = "found additional items"; _context->pushNewFrame(*expr); - if (auto attributeValue = getValueForArrayKeywordExpressionIfShouldGenerateError(*expr)) { + if (auto attributeValue = + getValueForKeywordExpressionIfShouldGenerateError(*expr, {BSONType::Array})) { appendErrorDetails(*expr); appendErrorReason(normalReason, ""); appendAdditionalItems(BSONArray(attributeValue.embeddedObject()), expr->startIndex()); @@ -1575,7 +1642,8 @@ private: } invariant(expr.getChild(0)->matchType() == MatchExpression::MatchType::INTERNAL_SCHEMA_MATCH_ARRAY_INDEX); - if (getValueForArrayKeywordExpressionIfShouldGenerateError(*expr.getChild(0))) { + if (getValueForKeywordExpressionIfShouldGenerateError(*expr.getChild(0), + {BSONType::Array})) { appendOperatorName(expr); // Since the "items" keyword set to an array of subschemas logically behaves as "$and", @@ -1615,7 +1683,8 @@ private: const std::string& normalReason, const std::string& invertedReason) { _context->pushNewFrame(*expr); - if (auto attributeValue = getValueForArrayKeywordExpressionIfShouldGenerateError(*expr)) { + if (auto attributeValue = + getValueForKeywordExpressionIfShouldGenerateError(*expr, {BSONType::Array})) { appendOperatorName(*expr); appendErrorReason(normalReason, invertedReason); auto failingElement = @@ -1819,6 +1888,8 @@ public: {"_propertiesExistList", {"", ""}}, {"items", {"details", ""}}, {"dependencies", {"failingDependencies", ""}}, + {"required", {"missingProperties", ""}}, + {"_property", {"details", ""}}, {"", {"details", ""}}}; auto detailsStringPair = detailsStringMap.find(tag); invariant(detailsStringPair != detailsStringMap.end()); @@ -2055,6 +2126,19 @@ private: void postVisitTreeOperator(const ListOfMatchExpression* expr, const std::string& detailsString) { finishLogicalOperatorChildError(expr, _context); + // If this node represents a 'properties' keyword or an individual property schema (denoted + // by '_property') and the current array builder has no elements, then this node will not + // contribute to the error output. As an example, consider the document {} against the + // following schema: {required: ['a'], properties: {'a': {minimum: 2, type: 'int'}}}. + // Though the AND representing 'properties' will fail and as such, is expected to construct + // an error, its children will not contribute to the generated error. As such, we + // retroactively mark an AND representing a 'properties' keyword or an individual + // 'property' as 'RuntimeState::kNoError' if no error details were produced. + auto tag = expr->getErrorAnnotation()->tag; + if (_context->shouldGenerateError(*expr) && (tag == "properties" || tag == "_property") && + _context->getCurrentArrayBuilder().arrSize() == 0) { + _context->setCurrentRuntimeState(RuntimeState::kNoError); + } // Append the result of the current array builder to the current object builder under the // field name 'detailsString' unless this node produces an array (i.e. in the case of a // subschema). @@ -2069,18 +2153,18 @@ private: }; /** - * Returns true if each node in the tree rooted at 'validatorExpr' has an error annotation, false - * otherwise. + * Verifies that each node in the tree rooted at 'validatorExpr' has an error annotation. */ -bool hasErrorAnnotations(const MatchExpression& validatorExpr) { - if (!validatorExpr.getErrorAnnotation()) - return false; +void assertHasErrorAnnotations(const MatchExpression& validatorExpr) { + uassert(4994600, + str::stream() << "Cannot generate validation error details: no annotation found for " + "expression " + << validatorExpr.toString(), + validatorExpr.getErrorAnnotation()); for (const auto childExpr : validatorExpr) { - if (!childExpr || !hasErrorAnnotations(*childExpr)) { - return false; - } + if (childExpr) + assertHasErrorAnnotations(*childExpr); } - return true; } /** @@ -2134,13 +2218,9 @@ BSONObj generateErrorHelper(const MatchExpression& validatorExpr, ValidationErrorInVisitor inVisitor{&context}; ValidationErrorPostVisitor postVisitor{&context}; - // TODO SERVER-49446: Once all nodes have ErrorAnnotations, this check should be converted to an - // invariant check that all nodes have an annotation. Also add an invariant to the - // DocumentValidationFailureInfo constructor to check that it is initialized with a non-empty - // object. - if (!hasErrorAnnotations(validatorExpr)) { - return BSONObj(); - } + // Verify that all nodes have error annotations. + assertHasErrorAnnotations(validatorExpr); + MatchExpressionWalker walker{&preVisitor, &inVisitor, &postVisitor}; tree_walker::walk<true, MatchExpression>(&validatorExpr, &walker); diff --git a/src/mongo/db/matcher/doc_validation_error.h b/src/mongo/db/matcher/doc_validation_error.h index e610138de21..62d86289cd6 100644 --- a/src/mongo/db/matcher/doc_validation_error.h +++ b/src/mongo/db/matcher/doc_validation_error.h @@ -44,7 +44,9 @@ class DocumentValidationFailureInfo final : public ErrorExtraInfo { public: static constexpr auto code = ErrorCodes::DocumentValidationFailure; static std::shared_ptr<const ErrorExtraInfo> parse(const BSONObj& obj); - explicit DocumentValidationFailureInfo(const BSONObj& err) : _details(err.getOwned()) {} + explicit DocumentValidationFailureInfo(const BSONObj& err) : _details(err.getOwned()) { + invariant(!err.isEmpty()); + } const BSONObj& getDetails() const; void serialize(BSONObjBuilder* bob) const override; diff --git a/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp b/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp index d4b391fdfac..7712ab9827a 100644 --- a/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp +++ b/src/mongo/db/matcher/doc_validation_error_json_schema_test.cpp @@ -51,7 +51,170 @@ TEST(JSONSchemaValidation, BasicProperties) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +// minimum +TEST(JSONSchemaValidation, MinimumNonNumericWithType) { + BSONObj query = + fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'number','minimum': 1}}}}"); + BSONObj document = fromjson("{'a': 'foo'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" + " {operatorName: 'properties', 'propertiesNotSatisfied': [" + " {propertyName: 'a', 'details': [" + " {'operatorName': 'type', " + " 'specifiedAs': { 'type': 'number' }, " + " 'reason': 'type did not match', " + " 'consideredValue': 'foo', " + " 'consideredType': 'string'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MinimumNonNumericWithBSONType) { + BSONObj query = + fromjson("{'$jsonSchema': {'properties': {'a': {'bsonType': 'int','minimum': 1}}}}"); + BSONObj document = fromjson("{'a': 1.1}"); + // The value satisfies the minimum keyword, but not the bsonType keyword. + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" + " {operatorName: 'properties', 'propertiesNotSatisfied': [" + " {propertyName: 'a', 'details': [" + " {'operatorName': 'bsonType', " + " 'specifiedAs': {'bsonType': 'int'}, " + " 'reason': 'type did not match', " + " 'consideredValue': 1.1, " + " 'consideredType': 'double'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MinimumWithRequiredNoTypeMissingProperty) { + BSONObj query = + fromjson("{'$jsonSchema': {'required': ['a'], 'properties': {'a': {'minimum': 1}}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'minimum'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MinimumWithRequiredAndTypeMissingProperty) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'required': ['a']," + " 'properties': " + " {a: {'type': 'number','minimum': 1}}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'minimum', nor 'type'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MinimumRequiredWithTypeAndScalarFailedMinimum) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'properties': {'a': {minimum: 2, 'bsonType': 'int'}}, 'required': ['a','b']}}"); + BSONObj document = fromjson("{a: 1, b: 1}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': " + " [{'operatorName': 'minimum'," + " 'specifiedAs': {'minimum' : 2}," + " 'reason': 'comparison failed'," + " 'consideredValue': 1}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +// maximum +TEST(JSONSchemaValidation, MaximumNonNumericWithType) { + BSONObj query = + fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'number','maximum': 1}}}}"); + BSONObj document = fromjson("{'a': 'foo'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" + " {operatorName: 'properties', 'propertiesNotSatisfied': [" + " {propertyName: 'a', 'details': [" + " {'operatorName': 'type', " + " 'specifiedAs': { 'type': 'number' }, " + " 'reason': 'type did not match', " + " 'consideredValue': 'foo', " + " 'consideredType': 'string'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MaximumNonNumericWithBSONType) { + BSONObj query = + fromjson("{'$jsonSchema': {'properties': {'a': {'bsonType': 'int','maximum': 1}}}}"); + BSONObj document = fromjson("{'a': 0.9}"); + // The value satisfies the maximum keyword, but not the bsonType keyword. + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" + " {operatorName: 'properties', 'propertiesNotSatisfied': [" + " {propertyName: 'a', 'details': [" + " {'operatorName': 'bsonType', " + " 'specifiedAs': {'bsonType': 'int'}, " + " 'reason': 'type did not match', " + " 'consideredValue': 0.9, " + " 'consideredType': 'double'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MaximumWithRequiredNoTypeMissingProperty) { + BSONObj query = + fromjson("{'$jsonSchema': {'required': ['a'], 'properties': {'a': {'maximum': 1}}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'maximum'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MaximumWithRequiredAndTypeMissingProperty) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'required': ['a']," + " 'properties': " + " {a: {'type': 'number','maximum': 1}}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'maximum', nor 'type'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MaximumRequiredWithTypeAndScalarFailedMaximum) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'properties': {'a': {maximum: 2, 'bsonType': 'int'}}, 'required': ['a','b']}}"); + BSONObj document = fromjson("{a: 3, b: 1}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': " + " [{'operatorName': 'maximum'," + " 'specifiedAs': {'maximum' : 2}," + " 'reason': 'comparison failed'," + " 'consideredValue': 3}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + // Exclusive minimum/maximum + TEST(JSONSchemaValidation, ExclusiveMinimum) { BSONObj query = fromjson( "{'$jsonSchema': {'properties': {'a': {'minimum': 1, 'exclusiveMinimum': true}}}}}"); @@ -130,7 +293,6 @@ TEST(JSONSchemaValidation, ExclusiveMaximum) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } -// TODO: Update the test when SERVER-50859 is implemented. TEST(JSONSchemaValidation, MaximumTypeNumberWithEmptyArray) { BSONObj query = fromjson("{'$jsonSchema': {'properties': {'a': {'maximum': 1, type: 'number'}}}}"); @@ -140,12 +302,8 @@ TEST(JSONSchemaValidation, MaximumTypeNumberWithEmptyArray) { " 'schemaRulesNotSatisfied': [" " {'operatorName': 'properties'," " 'propertiesNotSatisfied': [" - " {'propertyName': 'a', 'details': " - " [{'operatorName': 'maximum'," - " 'specifiedAs': {'maximum' : 1}," - " 'reason': 'comparison failed'," - " 'consideredValues': []}," - " {'operatorName': 'type'," + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'type'," " 'specifiedAs': {'type': 'number'}," " 'reason': 'type did not match'," " 'consideredValue': []," @@ -543,12 +701,6 @@ TEST(JSONSchemaValidation, MinLengthNonString) { "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" " {'operatorName': 'properties', 'propertiesNotSatisfied': [" " {'propertyName': 'a', 'details': [" - " {'operatorName': 'minLength', " - " 'specifiedAs': { 'minLength': 4 }, " - " 'reason': 'type did not match', " - " 'consideredType': 'int', " - " 'expectedType': 'string', " - " 'consideredValue': 1 }, " " {'operatorName': 'type', " " 'specifiedAs': { 'type': 'string' }, " " 'reason': 'type did not match', " @@ -557,6 +709,54 @@ TEST(JSONSchemaValidation, MinLengthNonString) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, MinLengthRequiredNoTypeMissingProperty) { + BSONObj query = + fromjson("{'$jsonSchema': {'required': ['a'], 'properties': {'a': {'minLength': 1}}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'minLength'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MinLengthWithRequiredAndTypeMissingProperty) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'required': ['a']," + " 'properties': " + " {a: {'type': 'string','minLength': 1}}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'minimum', nor 'type'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MinLengthRequiredWithTypeAndScalarFailedMinLength) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'properties': {'a': " + " {minLength: 2, 'bsonType': 'string'}}, 'required': ['a','b']}}"); + BSONObj document = fromjson("{a: 'a', b: 1}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': " + " [{'operatorName': 'minLength'," + " 'specifiedAs': {'minLength' : 2}," + " 'reason': 'specified string length was not satisfied'," + " 'consideredValue': 'a'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, MinLengthNested) { BSONObj query = fromjson( "{'$jsonSchema': {" @@ -637,6 +837,54 @@ TEST(JSONSchemaValidation, BasicMaxLength) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, MaxLengthRequiredNoTypeMissingProperty) { + BSONObj query = + fromjson("{'$jsonSchema': {'required': ['a'], 'properties': {'a': {'minLength': 1}}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'minLength'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MaxLengthWithRequiredAndTypeMissingProperty) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'required': ['a']," + " 'properties': " + " {a: {'type': 'string','minLength': 1}}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'maxLength', nor 'type'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MaxLengthRequiredWithTypeAndScalarFailedMaxLength) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'properties': {'a': " + " {maxLength: 2, 'bsonType': 'string'}}, 'required': ['a','b']}}"); + BSONObj document = fromjson("{a: 'aaaa', b: 1}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties'," + " 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': " + " [{'operatorName': 'maxLength'," + " 'specifiedAs': {'maxLength' : 2}," + " 'reason': 'specified string length was not satisfied'," + " 'consideredValue': 'aaaa'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, MaxLengthNoExplicitType) { BSONObj query = fromjson("{'$jsonSchema': {'properties': {'a': {'maxLength': 4}}}}"); BSONObj document = fromjson("{'a': 'foo, bar, baz'}"); @@ -660,12 +908,6 @@ TEST(JSONSchemaValidation, MaxLengthNonString) { "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" " {'operatorName': 'properties', 'propertiesNotSatisfied': [" " {'propertyName': 'a', 'details': [" - " {'operatorName': 'maxLength', " - " 'specifiedAs': { 'maxLength': 4 }, " - " 'reason': 'type did not match', " - " 'consideredType': 'int', " - " 'expectedType': 'string', " - " 'consideredValue': 1 }, " " {'operatorName': 'type', " " 'specifiedAs': { 'type': 'string' }, " " 'reason': 'type did not match', " @@ -769,20 +1011,14 @@ TEST(JSONSchemaValidation, PatternNoExplicitType) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } -TEST(JSONSchemaValidation, PatternNonString) { +TEST(JSONSchemaValidation, PatternNonStringWithType) { BSONObj query = fromjson("{'$jsonSchema': {'properties': {'a': {'type': 'string','pattern': '^S'}}}}"); BSONObj document = fromjson("{'a': 1}"); BSONObj expectedError = fromjson( "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" - " { operatorName: 'properties', 'propertiesNotSatisfied': [" + " {operatorName: 'properties', 'propertiesNotSatisfied': [" " {propertyName: 'a', 'details': [" - " {'operatorName': 'pattern'," - " 'specifiedAs': { pattern: '^S' }, " - " 'reason': 'type did not match', " - " 'consideredType': 'int', " - " 'expectedTypes': [ 'regex', 'string', 'symbol' ], " - " 'consideredValue': 1 }, " " {'operatorName': 'type', " " 'specifiedAs': { 'type': 'string' }, " " 'reason': 'type did not match', " @@ -791,6 +1027,50 @@ TEST(JSONSchemaValidation, PatternNonString) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, PatternWithRequiredNoTypeMissingProperty) { + BSONObj query = + fromjson("{'$jsonSchema': {'required': ['a'], 'properties': {'a': {'pattern': '^S'}}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'pattern'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, PatternWithRequiredAndTypeMissingProperty) { + BSONObj query = fromjson( + "{'$jsonSchema': {'required': ['a']," + " 'properties': {'a': {'type': 'string', 'pattern': '^S'}}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'pattern', nor 'type'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, PatternWithRequiredAndTypePatternFails) { + BSONObj query = fromjson( + "{'$jsonSchema': {'required': ['a']," + " 'properties': {'a': {'type': 'string', 'pattern': '^S'}}}}"); + BSONObj document = fromjson("{a: 'floo'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'pattern'," + " 'specifiedAs': {'pattern': '^S'}," + " 'reason': 'regular expression did not match'," + " 'consideredValue': 'floo'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, PatternNested) { BSONObj query = fromjson( "{'$jsonSchema': {" @@ -897,12 +1177,6 @@ TEST(JSONSchemaValidation, MultipleOfNonNumeric) { "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" " {'operatorName': 'properties', 'propertiesNotSatisfied': [" " {'propertyName': 'a', 'details': [" - " {'operatorName': 'multipleOf', " - " 'specifiedAs': { 'multipleOf': 2.1 }, " - " 'reason': 'type did not match', " - " 'consideredType': 'string', " - " 'expectedTypes': ['decimal', 'double', 'int', 'long'], " - " 'consideredValue': 'foo' }, " " {'operatorName': 'type', " " 'specifiedAs': { 'type': 'number' }, " " 'reason': 'type did not match', " @@ -911,6 +1185,49 @@ TEST(JSONSchemaValidation, MultipleOfNonNumeric) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, MultipleOfWithRequiredNoTypeMissingProperty) { + BSONObj query = + fromjson("{'$jsonSchema': {'required': ['a'], 'properties': {'a': {'multipleOf': 2}}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'pattern'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MultipleOfWithRequiredAndTypeMissingProperty) { + BSONObj query = + fromjson("{'$jsonSchema': {'required': ['a'], 'properties': {'a': {'multipleOf': 2}}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'pattern', nor 'type'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, MultipleOfWithRequiredAndTypeMultipleOfFails) { + BSONObj query = fromjson( + "{'$jsonSchema': {'required': ['a']," + " 'properties': {'a': {'type': 'number', 'multipleOf': 2}}}}"); + BSONObj document = fromjson("{a: 3}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + "'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'multipleOf'," + " 'specifiedAs': {'multipleOf': 2}," + " 'reason': 'considered value is not a multiple of the specified value'," + " 'consideredValue': 3}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, MultipleOfNested) { BSONObj query = fromjson( "{'$jsonSchema': {" @@ -1414,6 +1731,19 @@ TEST(JSONSchemaLogicalKeywordValidation, BasicEnum) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaLogicalKeywordValidation, EnumWithRequiredMissingProperty) { + BSONObj query = + fromjson("{'$jsonSchema': {'required': ['a'], 'properties': {'a': {'enum':[1,2,3]}}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaLogicalKeywordValidation, TopLevelEnum) { BSONObj query = fromjson("{'$jsonSchema': {'enum': [{'a': 1, 'b': 1}, {'a': 0, 'b': {'c': [1,2,3]}}]}}"); @@ -1516,6 +1846,22 @@ TEST(JSONSchemaValidation, ArrayMinItemsTypeArray) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, ArrayMinItemsTypeArrayRequiredMissingProperty) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'required': ['a']," + " 'properties': " + " {'a': {'type': 'array', 'minItems': 2}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, ArrayMinItems) { BSONObj query = fromjson( " {'$jsonSchema':" @@ -1534,6 +1880,22 @@ TEST(JSONSchemaValidation, ArrayMinItems) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, ArrayMinItemsRequiredMissingProperty) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'required': ['a']," + " 'properties': " + " {'a': {'minItems': 2}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, ArrayMinItemsAlwaysTrue) { BSONObj query = fromjson("{$nor: [{'$jsonSchema': {'minItems': 2}}]}"); BSONObj document = fromjson("{}"); @@ -1581,6 +1943,21 @@ TEST(JSONSchemaValidation, ArrayMaxItems) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, ArrayMaxItemsRequiredMissingProperty) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'required': ['a']," + " 'properties': {'a': {'maxItems': 2}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, ArrayMaxItemsAlwaysTrue) { BSONObj query = fromjson("{$nor: [{'$jsonSchema': {'maxItems': 2}}]}"); BSONObj document = fromjson("{}"); @@ -1610,6 +1987,21 @@ TEST(JSONSchemaValidation, ArrayUniqueItems) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, ArrayUniqueItemsRequiredMissingProperty) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'required': ['a'], 'properties': " + " {'a': {'uniqueItems': true}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, ArrayUniqueItemTypeArray) { BSONObj query = fromjson( " {'$jsonSchema':" @@ -1629,6 +2021,21 @@ TEST(JSONSchemaValidation, ArrayUniqueItemTypeArray) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, ArrayUniqueItemTypeArrayRequiredMissingProperty) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'required': ['a']," + " 'properties': {'a': {'type': 'array', 'uniqueItems': true}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, ArrayUniqueItemsTypeArrayOnNonArrayAttribute) { BSONObj query = fromjson( " {'$jsonSchema':" @@ -1675,6 +2082,21 @@ TEST(JSONSchemaValidation, ArrayItemsSingleSchema) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, ArrayItemsSingleSchemaRequiredMissingProperty) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'required': ['a'], 'properties': " + " {'a': {'items': {'type': 'string'}}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, ArrayItemsSingleSchemaTypeArrayOnNonArrayAttribute) { BSONObj query = fromjson( " {'$jsonSchema':" @@ -1694,6 +2116,21 @@ TEST(JSONSchemaValidation, ArrayItemsSingleSchemaTypeArrayOnNonArrayAttribute) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, ArrayItemsSingleSchemaTypeArrayRequiredMissingProperty) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'required': ['a'], 'properties': " + " {'a': {'items': {'type': 'string'}, 'type': 'array'}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + // Verifies that "items" with a single schema does not produce any unwanted artifacts when it does // not fail. We use "minItems" that fails validation to check that. TEST(JSONSchemaValidation, ArrayItemsSingleSchemaCombinedWithMinItems) { @@ -1742,6 +2179,24 @@ TEST(JSONSchemaValidation, ArrayItemsSingleSchemaNested) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, ArrayItemsSingleSchemaNestedRequiredMissingProperty) { + BSONObj query = fromjson( + "{'$jsonSchema': {'properties': " + " {'a': {'items': {'required': ['b'], 'properties': {'b': {'minItems': 2}}}}}}}"); + BSONObj document = fromjson("{'a': [{}]}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'items'," + " 'reason': 'At least one item did not match the sub-schema'," + " 'itemIndex': 0, " + " 'details':[" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['b']},'missingProperties': ['b']}]}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, ArrayItemsSingleSchema2DArray) { BSONObj query = fromjson("{'$jsonSchema': {'properties': {'a': {'items': {'items': {'minimum': 0}}}}}}"); @@ -1795,6 +2250,21 @@ TEST(JSONSchemaValidation, ArrayItemsSchemaArray) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, ArrayItemsSchemaArrayRequiredMissingProperty) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'required': ['a'], 'properties': " + " {'a': {'items': [{'type': 'number'}, {'type': 'string'}]}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + // Verifies that "items" with an array of schemas does not produce any unwanted artifacts when it // does not fail. We use "minItems" that fails validation to check that. TEST(JSONSchemaValidation, ArrayItemsSchemaArrayCombinedWithMinItems) { @@ -1855,6 +2325,21 @@ TEST(JSONSchemaValidation, ArrayItemsSchemaArrayTypeArrayOnNonArrayAttribute) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, ArrayItemsSchemaArrayTypeArrayRequiredMissingProperty) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'required': ['a'],'properties': " + " {'a': {'items': [{'type': 'number'}, {'type': 'string'}], 'type': 'array'}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + // Verifies that "items" with an empty array of schemas does not produce any unwanted artifacts. We // use "minItems" that fails validation to check that. TEST(JSONSchemaValidation, ArrayItemsEmptySchemaArrayCombinedWithMinItems) { @@ -1913,6 +2398,22 @@ TEST(JSONSchemaValidation, ArrayAdditionalItemsSchema) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, ArrayAdditionalItemsRequiredMissingProperty) { + BSONObj query = fromjson( + " {'$jsonSchema':" + " {'required': ['a'], 'properties': " + " {'a': {'type': 'array', 'items': [{'type': 'number'}, {'type': 'string'}], " + "'additionalItems': {'type': 'object'}}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, ArrayAdditionalItemsSchemaItemsAndItemsSchemaFail) { BSONObj query = fromjson( " {'$jsonSchema':" @@ -2030,6 +2531,22 @@ TEST(JSONSchemaValidation, ArrayAdditionalItemsFalse) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, ArrayAdditionalItemsFalseRequiredMissingProperty) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'required': ['a'], 'properties': {'a': " + " {'items': [{'type': 'number'}, {'type': 'string'}], " + "'additionalItems': false}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, ArrayAdditionalItemsFalseTypeArrayOnNonArrayAttribute) { BSONObj query = fromjson( "{'$jsonSchema': " @@ -2127,6 +2644,38 @@ TEST(JSONSchemaValidation, NestedMinPropertiesTypeMismatch) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, NestedMinPropertiesWithRequiredMissingProperty) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'required': ['a'],'properties': {'a': {type: 'object', minProperties: 10}}}}}"); + BSONObj document = fromjson("{}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, NestedMinPropertiesWithRequiredNonObject) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'required': ['a'],'properties': {'a': {type: 'object', minProperties: 10}}}}}"); + BSONObj document = fromjson("{a: []}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'a', 'details': [" + " {'operatorName': 'type'," + " 'specifiedAs': {'type': 'object'}," + " 'reason': 'type did not match'," + " 'consideredValue': []," + " 'consideredType': 'array'}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + // maxProperties TEST(JSONSchemaValidation, BasicMaxProperties) { BSONObj query = fromjson("{'$jsonSchema': {maxProperties: 2}}"); @@ -2238,6 +2787,31 @@ TEST(JSONSchemaValidation, PropertyFailingBiconditionalDependency) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, PropertyDependencyWithRequiredMissingProperty) { + BSONObj query = + fromjson("{'$jsonSchema': {'required': ['a'], 'dependencies': {'a': ['b', 'c']}}}"); + BSONObj document = fromjson("{}"); + // Should not mention 'dependencies'. + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'required'," + " specifiedAs: {required: ['a']}, " + " missingProperties: ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, PropertyDependencyWithRequiredMissingDependency) { + BSONObj query = + fromjson("{'$jsonSchema': {'required': ['a'], 'dependencies': {'a': ['b', 'c']}}}"); + BSONObj document = fromjson("{'a': 1}"); + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'dependencies', failingDependencies: [" + " {conditionalProperty: 'a', " + " missingProperties: ['b', 'c']}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + // schema dependencies TEST(JSONSchemaValidation, BasicSchemaDependency) { BSONObj query = fromjson( @@ -2279,6 +2853,39 @@ TEST(JSONSchemaValidation, NestedSchemaDependency) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, SchemaDependencyWithRequiredMissingProperty) { + BSONObj query = fromjson( + "{'$jsonSchema': {'required': ['a'], 'dependencies': {'a': {'properties': {'b': {'type': " + "'number'}}}}}}"); + BSONObj document = fromjson("{'b': 'foo'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, SchemaDependencyWithRequiredFailedDependency) { + BSONObj query = fromjson( + "{'$jsonSchema': " + "{'required': ['a'], 'dependencies': {'a': {'properties': {'b': {'type': 'number'}}}}}}"); + BSONObj document = fromjson("{a: 1, 'b': 'foo'}"); + BSONObj expectedError = fromjson( + "{operatorName: '$jsonSchema', schemaRulesNotSatisfied: [" + " {operatorName: 'dependencies', failingDependencies: [" + " {conditionalProperty: 'a', details: [" + " {operatorName: 'properties',propertiesNotSatisfied: [" + " {propertyName: 'b', details: [" + " {operatorName: 'type', " + " specifiedAs: {type: 'number'}, " + " reason: 'type did not match', " + " consideredValue:'foo'," + " consideredType: 'string'}]}]}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, BiconditionalSchemaDependency) { BSONObj query = fromjson( "{'$jsonSchema': {'dependencies': " @@ -2401,6 +3008,22 @@ TEST(JSONSchemaValidation, BasicAdditionalPropertiesFalse) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, BasicAdditionalPropertiesFalseRequiredMissingProperty) { + BSONObj query = fromjson( + "{'$jsonSchema': {'required': ['a'], 'properties': {'b': {'type': 'number'}}, " + "'additionalProperties': false}}}"); + BSONObj document = fromjson("{'_id': 0, 'b': 2}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" + " {'operatorName': 'additionalProperties', " + " 'specifiedAs': {additionalProperties: false}," + " 'additionalProperties': ['_id']}," + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, AdditionalPropertiesTrueProducesNoError) { BSONObj query = fromjson("{'$jsonSchema': {'minProperties': 100, 'additionalProperties': true}}"); @@ -2471,6 +3094,29 @@ TEST(JSONSchemaValidation, BasicAdditionalPropertiesSchema) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, BasicAdditionalPropertiesSchemaRequiredMissingProperty) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'required': ['a', 'b'], 'properties': {'a': {'type': 'number'}}, " + " 'additionalProperties': {'type': 'string'}}}}"); + BSONObj document = fromjson("{'a': 1, 'c': 'not this one, but the next one', 'd': 1 }"); + // Should produce an error for 'd' and 'b'. + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema', 'schemaRulesNotSatisfied': [" + " {'operatorName': 'additionalProperties'," + " 'reason':'at least one additional property did not match the subschema'," + " 'failingProperty': 'd', 'details': [ " + " {'operatorName': 'type', " + " 'specifiedAs': {type: 'string'}," + " 'reason': 'type did not match'," + " 'consideredValue': 1," + " 'consideredType': 'int'}]}," + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a','b']}," + " 'missingProperties': ['b']}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, BasicAdditionalPropertiesSchemaNested) { BSONObj query = fromjson( "{'$jsonSchema': " @@ -2528,6 +3174,20 @@ TEST(JSONSchemaValidation, BasicPatternProperties) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +TEST(JSONSchemaValidation, BasicPatternPropertiesRequired) { + BSONObj query = fromjson( + "{'$jsonSchema': " + " {'required': ['Super'], 'patternProperties': {'^S': {'type':'number'}}}}"); + BSONObj document = fromjson("{'super': 1, 'slow': '67'}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['Super']}," + " 'missingProperties': ['Super']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + TEST(JSONSchemaValidation, NestedPatternProperties) { BSONObj query = fromjson( "{'$jsonSchema': {'properties': {'a': {'patternProperties': {'^S': {'type': " @@ -2798,5 +3458,48 @@ TEST(JSONSchemaValidation, PatternPropertiesAndAdditionalPropertiesSchemaNeither " 'numberOfProperties': 4}]}"); doc_validation_error::verifyGeneratedError(query, document, expectedError); } + +// required +TEST(JSONSchemaValidation, BasicRequired) { + BSONObj query = fromjson("{'$jsonSchema': {required: ['a','b','c']}}"); + BSONObj document = fromjson("{'c': 1, 'd': 2}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a','b','c']}," + " 'missingProperties': ['a', 'b']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, RequiredMixedWithProperties) { + BSONObj query = fromjson( + "{'$jsonSchema': {'properties': {'a': {minimum: 2}, 'd': {maximum: 5}}, 'required': " + "['a','b','c']}}}"); + BSONObj document = fromjson("{'c': 1, 'b': 2}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a','b','c']}," + " 'missingProperties': ['a']}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + +TEST(JSONSchemaValidation, RequiredNested) { + BSONObj query = + fromjson("{'$jsonSchema': {'properties': {'topLevelField': {'required': ['a','b','c']}}}}"); + BSONObj document = fromjson("{'topLevelField': {'c': 1, 'd': 2}}"); + BSONObj expectedError = fromjson( + "{'operatorName': '$jsonSchema'," + " 'schemaRulesNotSatisfied': [" + " {'operatorName': 'properties', 'propertiesNotSatisfied': [" + " {'propertyName': 'topLevelField', 'details': [" + " {'operatorName': 'required'," + " 'specifiedAs': {'required': ['a','b','c']}," + " 'missingProperties': ['a', 'b']}]}]}]}"); + doc_validation_error::verifyGeneratedError(query, document, expectedError); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/matcher/schema/json_schema_parser.cpp b/src/mongo/db/matcher/schema/json_schema_parser.cpp index 86f3139b5fd..ec980d71ebc 100644 --- a/src/mongo/db/matcher/schema/json_schema_parser.cpp +++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp @@ -498,14 +498,20 @@ StatusWith<StringDataSet> parseRequired(BSONElement requiredElt) { */ StatusWithMatchExpression translateRequired(const boost::intrusive_ptr<ExpressionContext>& expCtx, const StringDataSet& requiredProperties, + BSONElement requiredElt, StringData path, InternalSchemaTypeExpression* typeExpr) { - auto andExpr = std::make_unique<AndMatchExpression>(); + auto andExpr = std::make_unique<AndMatchExpression>( + doc_validation_error::createAnnotation(expCtx, "required", requiredElt.wrap())); std::vector<StringData> sortedProperties(requiredProperties.begin(), requiredProperties.end()); std::sort(sortedProperties.begin(), sortedProperties.end()); for (auto&& propertyName : sortedProperties) { - andExpr->add(new ExistsMatchExpression(propertyName)); + // This node is tagged as '_propertyExists' to indicate that it will produce a path instead + // of a detailed BSONObj error during error generation. + andExpr->add(new ExistsMatchExpression( + propertyName, + doc_validation_error::createAnnotation(expCtx, "_propertyExists", BSONObj()))); } // If this is a top-level schema, then we know that we are matching against objects, and there @@ -514,8 +520,10 @@ StatusWithMatchExpression translateRequired(const boost::intrusive_ptr<Expressio return {std::move(andExpr)}; } - auto objectMatch = - std::make_unique<InternalSchemaObjectMatchExpression>(path, std::move(andExpr)); + auto objectMatch = std::make_unique<InternalSchemaObjectMatchExpression>( + path, + std::move(andExpr), + doc_validation_error::createAnnotation(expCtx, AnnotationMode::kIgnoreButDescend)); return makeRestriction(expCtx, BSONType::Object, path, std::move(objectMatch), typeExpr); } @@ -554,7 +562,9 @@ StatusWithMatchExpression parseProperties(const boost::intrusive_ptr<ExpressionC } nestedSchemaMatch.getValue()->setErrorAnnotation(doc_validation_error::createAnnotation( - expCtx, "", BSON("propertyName" << property.fieldNameStringData().toString()))); + expCtx, + "_property", + BSON("propertyName" << property.fieldNameStringData().toString()))); if (requiredProperties.find(property.fieldNameStringData()) != requiredProperties.end()) { // The field name for which we created the nested schema is a required property. This // property must exist and therefore must match 'nestedSchemaMatch'. @@ -787,7 +797,7 @@ StatusWithMatchExpression makeDependencyExistsClause( StringData path, StringData dependencyName) { // This node is tagged as '_propertyExists' to indicate that it will produce a path instead - // of a detail BSONObj error during error generation. + // of a detailed BSONObj error during error generation. auto existsExpr = std::make_unique<ExistsMatchExpression>( dependencyName, doc_validation_error::createAnnotation(expCtx, "_propertyExists", BSONObj())); @@ -1357,7 +1367,11 @@ Status translateObjectKeywords(StringMap<BSONElement>& keywordMap, } if (!requiredProperties.empty()) { - auto requiredExpr = translateRequired(expCtx, requiredProperties, path, typeExpr); + auto requiredExpr = translateRequired(expCtx, + requiredProperties, + keywordMap[JSONSchemaParser::kSchemaRequiredKeyword], + path, + typeExpr); if (!requiredExpr.isOK()) { return requiredExpr.getStatus(); } |