diff options
author | Nicholas Zolnierz <nicholas.zolnierz@mongodb.com> | 2019-10-07 14:14:46 +0000 |
---|---|---|
committer | evergreen <evergreen@mongodb.com> | 2019-10-07 14:14:46 +0000 |
commit | f6b19b784598fb1143ecbe29bd7970ce645183f6 (patch) | |
tree | 4bc4b84ebeec2454e698b00b7d081633f4f5d0ba /src | |
parent | 27409fcff6f2ee860cda3341f37302e66334f716 (diff) | |
download | mongo-f6b19b784598fb1143ecbe29bd7970ce645183f6.tar.gz |
SERVER-43842 Break up json_schema_parser_test.cpp to fix -fvar-tracking-assignments note
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/db/matcher/SConscript | 6 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/array_keywords_test.cpp | 456 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/encrypt_keyword_test.cpp | 316 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser.cpp | 1 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/json_schema_parser_test.cpp | 2167 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/logical_keywords_test.cpp | 271 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/object_keywords_test.cpp | 939 | ||||
-rw-r--r-- | src/mongo/db/matcher/schema/scalar_keywords_test.cpp | 383 |
8 files changed, 2371 insertions, 2168 deletions
diff --git a/src/mongo/db/matcher/SConscript b/src/mongo/db/matcher/SConscript index 0bdd51b8c07..8aad3efe9db 100644 --- a/src/mongo/db/matcher/SConscript +++ b/src/mongo/db/matcher/SConscript @@ -118,6 +118,8 @@ env.CppUnitTest( 'path_accepting_keyword_test.cpp', 'path_test.cpp', 'rewrite_expr_test.cpp', + 'schema/array_keywords_test.cpp', + 'schema/encrypt_keyword_test.cpp', 'schema/encrypt_schema_types_test.cpp', 'schema/expression_internal_schema_all_elem_match_from_index_test.cpp', 'schema/expression_internal_schema_allowed_properties_test.cpp', @@ -137,7 +139,9 @@ env.CppUnitTest( 'schema/expression_internal_schema_xor_test.cpp', 'schema/expression_parser_schema_test.cpp', 'schema/json_pointer_test.cpp', - 'schema/json_schema_parser_test.cpp', + 'schema/logical_keywords_test.cpp', + 'schema/object_keywords_test.cpp', + 'schema/scalar_keywords_test.cpp', ], LIBDEPS=[ '$BUILD_DIR/mongo/db/query/collation/collator_interface_mock', diff --git a/src/mongo/db/matcher/schema/array_keywords_test.cpp b/src/mongo/db/matcher/schema/array_keywords_test.cpp new file mode 100644 index 00000000000..96edb138560 --- /dev/null +++ b/src/mongo/db/matcher/schema/array_keywords_test.cpp @@ -0,0 +1,456 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/bson/json.h" +#include "mongo/db/matcher/schema/json_schema_parser.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +#define ASSERT_SERIALIZES_TO(match, expected) \ + do { \ + BSONObjBuilder bob; \ + match->serialize(&bob); \ + ASSERT_BSONOBJ_EQ(bob.obj(), expected); \ + } while (false) + +TEST(JSONSchemaArrayKeywordTest, FailsToParseIfMinItemsIsNotANumber) { + auto schema = BSON("minItems" << BSON_ARRAY(1)); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaArrayKeywordTest, FailsToParseIfMinItemsIsNotANonNegativeInteger) { + auto schema = BSON("minItems" << -1); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::FailedToParse); + + schema = BSON("minItems" << 3.14); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaArrayKeywordTest, MinItemsTranslatesCorrectlyWithNoType) { + auto schema = BSON("minItems" << 1); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); + + schema = fromjson("{properties: {a: {minItems: 1}}}"); + result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{a: {$not: {$exists: true}}}, + {a: {$not: {$_internalSchemaType: [4]}}}, + {a: {$_internalSchemaMinItems: 1}}] + })")); +} + +TEST(JSONSchemaArrayKeywordTest, MinItemsTranslatesCorrectlyWithArrayType) { + auto schema = fromjson("{properties: {a: {minItems: 1, type: 'array'}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{a: {$not: {$exists: true}}}, + {$and: [ {a: {$_internalSchemaMinItems: 1}}, {a: {$_internalSchemaType: [4]}}]}] + })")); +} + +TEST(JSONSchemaArrayKeywordTest, MinItemsTranslatesCorrectlyWithNonArrayType) { + auto schema = fromjson("{properties: {a: {minItems: 1, type: 'number'}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{a: {$not: {$exists: true}}}, {a: {$_internalSchemaType: ["number"]}}] + })")); +} + +TEST(JSONSchemaArrayKeywordTest, FailsToParseIfMaxItemsIsNotANumber) { + auto schema = BSON("maxItems" << BSON_ARRAY(1)); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaArrayKeywordTest, FailsToParseIfMaxItemsIsNotANonNegativeInteger) { + auto schema = BSON("maxItems" << -1); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::FailedToParse); + + schema = BSON("maxItems" << 1.60217); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaArrayKeywordTest, MaxItemsTranslatesCorrectlyWithNoType) { + auto schema = BSON("maxItems" << 1); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); + + schema = fromjson("{properties: {a: {maxItems: 1}}}"); + result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{a: {$not: {$exists: true}}}, + {a: {$not: {$_internalSchemaType: [4]}}}, + {a: {$_internalSchemaMaxItems: 1}}] + })")); +} + +TEST(JSONSchemaArrayKeywordTest, MaxItemsTranslatesCorrectlyWithArrayType) { + auto schema = fromjson("{properties: {a: {maxItems: 1, type: 'array'}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{a: {$not: {$exists: true}}}, + {$and: [ {a: {$_internalSchemaMaxItems: 1}}, {a: {$_internalSchemaType: [4]}}]}] + })")); +} + +TEST(JSONSchemaArrayKeywordTest, MaxItemsTranslatesCorrectlyWithNonArrayType) { + auto schema = fromjson("{properties: {a: {maxItems: 1, type: 'string'}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{a: {$not: {$exists : true}}}, {a: {$_internalSchemaType: [2]}}] + })")); +} + +TEST(JSONSchemaArrayKeywordTest, FailsToParseIfUniqueItemsIsNotABoolean) { + auto schema = BSON("uniqueItems" << 1); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaArrayKeywordTest, UniqueItemsFalseGeneratesAlwaysTrueExpression) { + auto schema = fromjson("{properties: {a: {uniqueItems: false}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); +} + +TEST(JSONSchemaArrayKeywordTest, UniqueItemsTranslatesCorrectlyWithNoType) { + auto schema = BSON("uniqueItems" << true); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); + + schema = fromjson("{properties: {a: {uniqueItems: true}}}"); + result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{a: {$not: {$exists: true}}}, + {a: {$not: {$_internalSchemaType: [4]}}}, + {a: {$_internalSchemaUniqueItems: true}}] + })")); +} + +TEST(JSONSchemaArrayKeywordTest, UniqueItemsTranslatesCorrectlyWithTypeArray) { + auto schema = fromjson("{properties: {a: {type: 'array', uniqueItems: true}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{a: {$not: {$exists: true}}}, + {$and: [ {a: {$_internalSchemaUniqueItems: true}}, {a: {$_internalSchemaType: [4]}}]}] + })")); +} + +TEST(JSONSchemaArrayKeywordTest, FailsToParseIfItemsIsNotAnArrayOrObject) { + auto schema = BSON("items" << 1); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaArrayKeywordTest, FailsToParseIfItemsIsAnArrayWithANonObject) { + auto schema = fromjson("{items: [{type: 'string'}, 'blah']}"); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaArrayKeywordTest, FailsToParseIfItemsIsAnInvalidSchema) { + auto schema = BSON("items" << BSON("invalid" << 1)); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaArrayKeywordTest, FailsToParseIfItemsIsAnArrayThatContainsAnInvalidSchema) { + auto schema = fromjson("{items: [{type: 'string'}, {invalid: 1}]}"); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaArrayKeywordTest, ItemsParsesSuccessfullyAsArrayAtTopLevel) { + auto schema = fromjson("{items: [{type: 'string'}]}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); +} + +TEST(JSONSchemaArrayKeywordTest, ItemsParsesSuccessfullyAsObjectAtTopLevel) { + auto schema = fromjson("{items: {type: 'string'}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); +} + +TEST(JSONSchemaArrayKeywordTest, ItemsParsesSuccessfullyAsArrayInNestedSchema) { + auto schema = fromjson("{properties: {a: {items: [{maxLength: 4}, {minimum: 0}]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + auto expectedResult = fromjson(R"( + { + $or: + [{a: {$not: {$exists: true}}}, {a: {$not: {$_internalSchemaType: [4]}}}, { + $and: [ + { + a: { + $_internalSchemaMatchArrayIndex: { + index: 0, + namePlaceholder: "i", + expression: { + $or: [ + {i: {$not: {$_internalSchemaType: [2]}}}, + {i: {$_internalSchemaMaxLength: 4}} + ] + } + } + } + }, + { + a: { + $_internalSchemaMatchArrayIndex : { + index: 1, + namePlaceholder: "i", + expression: { + $or: [ + {i : {$not: {$_internalSchemaType: ["number"]}}}, + {i: {$gte: 0}} + ] + } + } + } + } + ] + }] + })"); + ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); +} + +TEST(JSONSchemaArrayKeywordTest, ItemsParsesSuccessfullyAsObjectInNestedSchema) { + auto schema = fromjson("{properties: {a: {items: {type: 'string'}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{a: {$not: {$exists: true}}}, + {a: {$not: {$_internalSchemaType: [4]}}}, + {a: {$_internalSchemaAllElemMatchFromIndex : [ 0, {i: {$_internalSchemaType: [2]}}]}}] + })")); +} + +TEST(JSONSchemaArrayKeywordTest, FailsToParseIfAdditionalItemsIsNotAnObjectOrBoolean) { + auto schema = BSON("items" << BSONObj() << "additionalItems" << 1); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::TypeMismatch); + + schema = BSON("additionalItems" << 1); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaArrayKeywordTest, FailsToParseIfAdditionalItemsIsAnInvalidSchema) { + auto schema = BSON("items" << BSONObj() << "additionalItems" << BSON("invalid" << 1)); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::FailedToParse); + + schema = BSON("additionalItems" << BSON("invalid" << 1)); + ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), + ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaArrayKeywordTest, AdditionalItemsTranslatesSucessfullyAsBooleanAtTopLevel) { + auto schema = fromjson("{items: [], additionalItems: true}"); + auto expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(expr.getStatus()); + auto optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); + ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); + + schema = fromjson("{items: [], additionalItems: false}"); + expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(expr.getStatus()); + optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); + ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); +} + +TEST(JSONSchemaArrayKeywordTest, AdditionalItemsTranslatesSucessfullyAsObjectAtTopLevel) { + auto schema = fromjson("{items: [], additionalItems: {multipleOf: 7}}"); + auto expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(expr.getStatus()); + auto optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); + ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); +} + +TEST(JSONSchemaArrayKeywordTest, AdditionalItemsTranslatesSucessfullyAsBooleanInNestedSchema) { + auto schema = fromjson("{properties: {a: {items: [], additionalItems: true}}}"); + auto expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(expr.getStatus()); + auto optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); + auto expectedResult = fromjson(R"( + { + $or: + [{a: {$not: {$exists: true}}}, + {a: {$not: {$_internalSchemaType: [4]}}}, + {a: {$_internalSchemaAllElemMatchFromIndex: [0, {$alwaysTrue: 1}]}}] + })"); + ASSERT_SERIALIZES_TO(optimizedExpr, expectedResult); + + schema = fromjson("{properties: {a: {items: [], additionalItems: false}}}"); + expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(expr.getStatus()); + optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); + expectedResult = fromjson(R"( + { + $or: + [{a: {$not: {$exists: true}}}, + {a: {$not: {$_internalSchemaType: [4]}}}, + {a: {$_internalSchemaAllElemMatchFromIndex: [0, {$alwaysFalse: 1}]}}] + })"); + ASSERT_SERIALIZES_TO(optimizedExpr, expectedResult); +} + +TEST(JSONSchemaArrayKeywordTest, + AdditionalItemsGeneratesEmptyExpressionAtTopLevelIfItemsNotPresent) { + auto schema = BSON("additionalItems" << true); + auto expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(expr.getStatus()); + auto optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); + ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); + + schema = BSON("additionalItems" << false); + expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(expr.getStatus()); + optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); + ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); + + schema = BSON("additionalItems" << BSON("minLength" << 1)); + expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(expr.getStatus()); + optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); + ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); +} + +TEST(JSONSchemaArrayKeywordTest, + AdditionalItemsGeneratesEmptyExpressionInNestedSchemaIfItemsNotPresent) { + auto schema = fromjson("{properties: {foo: {additionalItems: true}}}"); + auto expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(expr.getStatus()); + auto optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); + ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); + + + schema = fromjson("{properties: {foo: {additionalItems: false}}}"); + expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(expr.getStatus()); + optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); + ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); +} + +TEST(JSONSchemaArrayKeywordTest, AdditionalItemsGeneratesEmptyExpressionIfItemsAnObject) { + auto schema = fromjson("{properties: {a: {items: {minimum: 7}, additionalItems: false}}}"); + auto expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(expr.getStatus()); + auto optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); + auto expectedResult = fromjson(R"( + { + $or: + [{a: {$not: {$exists: true}}}, + {a: {$not: {$_internalSchemaType: [4]}}}, + { + a: { + $_internalSchemaAllElemMatchFromIndex: [ + 0, + {$or: [ {i: {$not: {$_internalSchemaType: ["number"]}}}, {i: {$gte: 7}}]} + ] + } + }] + })"); + ASSERT_SERIALIZES_TO(optimizedExpr, expectedResult); + + schema = fromjson("{properties: {a: {items: {minimum: 7}, additionalItems: {minLength: 7}}}}"); + expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(expr.getStatus()); + optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); + expectedResult = fromjson(R"( + { + $or: + [{a: {$not: {$exists: true}}}, + {a: {$not: {$_internalSchemaType: [4]}}}, + { + a: { + $_internalSchemaAllElemMatchFromIndex : [ + 0, + {$or: [ {i: {$not: {$_internalSchemaType: ["number"]}}}, {i: {$gte: 7}}]} + ] + } + }] + })"); + ASSERT_SERIALIZES_TO(optimizedExpr, expectedResult); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/encrypt_keyword_test.cpp b/src/mongo/db/matcher/schema/encrypt_keyword_test.cpp new file mode 100644 index 00000000000..ddc0e301d33 --- /dev/null +++ b/src/mongo/db/matcher/schema/encrypt_keyword_test.cpp @@ -0,0 +1,316 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/bson/json.h" +#include "mongo/db/matcher/expression_always_boolean.h" +#include "mongo/db/matcher/schema/json_schema_parser.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +#define ASSERT_SERIALIZES_TO(match, expected) \ + do { \ + BSONObjBuilder bob; \ + match->serialize(&bob); \ + ASSERT_BSONOBJ_EQ(bob.obj(), expected); \ + } while (false) + +TEST(JSONSchemaParserEncryptTest, EncryptTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {foo: {encrypt: {}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"( + { + $or: + [{foo: {$not: {$exists: true}}}, { + $and: + [ {foo: {$_internalSchemaBinDataSubType: 6}}, {foo: {$_internalSchemaType: [5]}} ] + }] + })")); +} + +TEST(JSONSchemaParserEncryptTest, EncryptWithSingleBsonTypeTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {foo: {encrypt: {bsonType: \"string\"}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"( + { + $or: + [{foo: {$not: {$exists: true}}}, { + $and: + [ {foo: {$_internalSchemaBinDataSubType: 6}}, + {foo: {$_internalSchemaBinDataEncryptedType: [2]}}, + {foo: {$_internalSchemaType: [5]}}] + }] + })")); +} + +TEST(JSONSchemaParserEncryptTest, EncryptWithArrayOfMultipleTypesTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {foo: {encrypt: {bsonType: [\"string\",\"date\"]}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"( + { + $or: + [{foo: {$not: {$exists: true}}}, { + $and: + [ {foo: {$_internalSchemaBinDataSubType: 6}}, + {foo: {$_internalSchemaBinDataEncryptedType: [2, 9]}}, + {foo: {$_internalSchemaType: [5]}}] + }] + })")); +} + +TEST(JSONSchemaParserEncryptTest, TopLevelEncryptTranslatesCorrectly) { + BSONObj schema = fromjson("{encrypt: {}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, BSON(AlwaysFalseMatchExpression::kName << 1)); +} + +TEST(JSONSchemaParserEncryptTest, NestedEncryptTranslatesCorrectly) { + BSONObj schema = + fromjson("{properties: {a: {type: 'object', properties: {b: {encrypt: {}}}}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"( + { + $or: + [{a: {$not: {$exists: true}}}, { + $and: [ + { + a: { + $_internalSchemaObjectMatch: { + $or: [ + {b: {$not : {$exists: true}}}, + { + $and: [ + {b: {$_internalSchemaBinDataSubType: 6}}, + {b: {$_internalSchemaType: [5]}} + ] + } + ] + } + } + }, + {a: {$_internalSchemaType: [3]}} + ] + }] + })")); +} + +TEST(JSONSchemaParserEncryptTest, NestedEncryptInArrayTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {a: {type: 'array', items: {encrypt: {}}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"( + { + $or: + [{a: {$not: {$exists: true}}}, { + $and: [ + { + a: { + $_internalSchemaAllElemMatchFromIndex : [ + 0, + { + $and: [ + {i: {$_internalSchemaBinDataSubType: 6}}, + {i: {$_internalSchemaType: [5]}} + ] + } + ] + } + }, + {a: {$_internalSchemaType: [4]}} + ] + }] + })")); +} + +TEST(JSONSchemaParserEncryptTest, FailsToParseIfBothEncryptAndTypeArePresent) { + BSONObj schema = fromjson("{encrypt: {}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserEncryptTest, FailsToParseIfBothEncryptAndBSONTypeArePresent) { + BSONObj schema = fromjson("{encrypt: {}, bsonType: 'binData'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserEncryptTest, FailsToParseIfEncryptValueIsNotObject) { + BSONObj schema = fromjson("{properties: {foo: {encrypt: 12}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaParserEncryptTest, ParseSucceedsWithEmptyEncryptObject) { + BSONObj schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSONObj()))); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserEncryptTest, ParseSucceedsWithBsonType) { + BSONObj schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("bsonType" + << "int")))); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserEncryptTest, ParseFailsWithBsonTypeGivenByCode) { + BSONObj schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("bsonType" << 5)))); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus().code(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaParserEncryptTest, ParseSucceedsWithArrayOfBsonTypes) { + BSONObj schema = + BSON("properties" << BSON( + "foo" << BSON("encrypt" << BSON("bsonType" << BSON_ARRAY("int" + << "date" + << "string"))))); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserEncryptTest, ParseSucceedsIfEncryptFieldsAreValid) { + auto schema = BSON( + "properties" << BSON( + "foo" << BSON("encrypt" << BSON("bsonType" + << "string" + << "keyId" + << "/pointer" + << "algorithm" + << "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")))); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserEncryptTest, FailsToParseIfEncryptHasBadFieldName) { + BSONObj schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("keyIdx" + << "/pointer")))); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus().code(), 40415); + schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("bsonType" + << "bool" + << "keyIdx" + << "/pointer")))); + result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus().code(), 40415); +} + +TEST(JSONSchemaParserEncryptTest, FailsToParseWithBadKeyIdArray) { + auto schema = BSON( + "properties" << BSON("foo" << BSON("encrypt" << BSON("keyId" << BSON_ARRAY("nonsense" + << "again"))))); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus().code(), 51088); +} + +TEST(JSONSchemaParserEncryptTest, FailsToParseWithBadBSONType) { + auto schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("bsonType" + << "Stringx")))); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus().code(), ErrorCodes::BadValue); + + schema = BSON("properties" << BSON( + "foo" << BSON("encrypt" << BSON("bsonType" << (BSONType::JSTypeMax + 1))))); + result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus().code(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaParserEncryptTest, FailsToParseWithBadAlgorithm) { + auto schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("algorithm" + << "Stringx")))); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus().code(), ErrorCodes::BadValue); +} + +TEST(JSONSchemaParserEncryptTest, FailsToParseWithBadPointer) { + auto schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("keyId" + << "invalidPointer")))); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus().code(), 51065); +} + +TEST(JSONSchemaParserEncryptTest, TopLevelEncryptMetadataValidatedCorrectly) { + BSONObj schema = fromjson( + "{encryptMetadata: {algorithm: \"AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic\"," + " keyId: [{$binary: \"ASNFZ4mrze/ty6mHZUMhAQ==\", $type: \"04\"}]}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserEncryptTest, NestedEncryptMetadataValidatedCorrectly) { + BSONObj schema = fromjson( + "{properties: {a: {encryptMetadata: {algorithm: \"AEAD_AES_256_CBC_HMAC_SHA_512-Random\", " + "keyId: [{$binary: \"ASNFZ4mrze/ty6mHZUMhAQ==\", $type: \"04\"}]}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); +} + +TEST(JSONSchemaParserEncryptTest, FailsToParseIfEncryptMetadataValueIsEmptyObject) { + BSONObj schema = fromjson("{encryptMetadata: {}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserEncryptTest, FailsToParseIfBothEncryptAndEncryptMetadataAreSiblings) { + BSONObj schema = fromjson( + "{encrypt: {}, encryptMetadata: {algorithm: " + "\"AEAD_AES_256_CBC_HMAC_SHA_512-Random\"}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserEncryptTest, FailsToParseWithNonUUIDArrayElement) { + BSONArrayBuilder builder; + UUID::gen().appendToArrayBuilder(&builder); + UUID::gen().appendToArrayBuilder(&builder); + builder.appendBinData(16, BinDataType::Encrypt, "16charactershere"); + auto schema = + BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("keyId" << builder.arr())))); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus().code(), 51084); +} + +} // 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 0608c2389b4..3d345aebf12 100644 --- a/src/mongo/db/matcher/schema/json_schema_parser.cpp +++ b/src/mongo/db/matcher/schema/json_schema_parser.cpp @@ -1207,6 +1207,7 @@ Status translateObjectKeywords(StringMap<BSONElement>& keywordMap, * - minLength * - maxLength * - pattern + * - multipleOf */ Status translateScalarKeywords(StringMap<BSONElement>& keywordMap, StringData path, diff --git a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp b/src/mongo/db/matcher/schema/json_schema_parser_test.cpp deleted file mode 100644 index 6a47dbe567a..00000000000 --- a/src/mongo/db/matcher/schema/json_schema_parser_test.cpp +++ /dev/null @@ -1,2167 +0,0 @@ -/** - * Copyright (C) 2018-present MongoDB, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Server Side Public License, version 1, - * as published by MongoDB, Inc. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * Server Side Public License for more details. - * - * You should have received a copy of the Server Side Public License - * along with this program. If not, see - * <http://www.mongodb.com/licensing/server-side-public-license>. - * - * As a special exception, the copyright holders give permission to link the - * code of portions of this program with the OpenSSL library under certain - * conditions as described in each individual source file and distribute - * linked combinations including the program with the OpenSSL library. You - * must comply with the Server Side Public License in all respects for - * all of the code used other than as permitted herein. If you modify file(s) - * with this exception, you may extend this exception to your version of the - * file(s), but you are not obligated to do so. If you do not wish to do so, - * delete this exception statement from your version. If you delete this - * exception statement from all source files in the program, then also delete - * it in the license file. - */ - -#include "mongo/platform/basic.h" - -#include "mongo/bson/bsonobjbuilder.h" -#include "mongo/bson/json.h" -#include "mongo/db/bson/bson_helper.h" -#include "mongo/db/matcher/expression_always_boolean.h" -#include "mongo/db/matcher/schema/json_schema_parser.h" -#include "mongo/db/pipeline/expression_context_for_test.h" -#include "mongo/db/query/query_knobs_gen.h" -#include "mongo/unittest/death_test.h" -#include "mongo/unittest/unittest.h" - -namespace mongo { -namespace { - -#define ASSERT_SERIALIZES_TO(match, expected) \ - do { \ - BSONObjBuilder bob; \ - match->serialize(&bob); \ - ASSERT_BSONOBJ_EQ(bob.obj(), expected); \ - } while (false) - -TEST(JSONSchemaParserTest, FailsToParseIfTypeIsNotAString) { - BSONObj schema = fromjson("{type: 1}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseNicelyIfTypeIsKnownUnsupportedAlias) { - BSONObj schema = fromjson("{type: 'integer'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema type 'integer' is not currently supported"); -} - -TEST(JSONSchemaParserTest, FailsToParseUnknownKeyword) { - BSONObj schema = fromjson("{unknown: 1}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfPropertiesIsNotAnObject) { - BSONObj schema = fromjson("{properties: 1}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseIfPropertiesIsNotAnObjectWithType) { - BSONObj schema = fromjson("{type: 'string', properties: 1}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseIfParticularPropertyIsNotAnObject) { - BSONObj schema = fromjson("{properties: {foo: 1}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseIfKeywordIsDuplicated) { - BSONObj schema = BSON("type" - << "object" - << "type" - << "object"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, EmptySchemaTranslatesCorrectly) { - BSONObj schema = fromjson("{}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); -} - -TEST(JSONSchemaParserTest, TypeObjectTranslatesCorrectly) { - BSONObj schema = fromjson("{type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); -} - -TEST(JSONSchemaParserTest, NestedTypeObjectTranslatesCorrectly) { - BSONObj schema = - fromjson("{properties: {a: {type: 'object', properties: {b: {type: 'string'}}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO( - optimizedResult, - fromjson("{$or: [{a: {$not: {$exists: true }}}, {$and: [{a: {$_internalSchemaObjectMatch: " - "{$or: [{b: {$not: {$exists: true}}}, {b: {$_internalSchemaType: [2]}}]}}}, {a: " - "{$_internalSchemaType: [3]}}]}]}")); -} - -TEST(JSONSchemaParserTest, TopLevelNonObjectTypeTranslatesCorrectly) { - BSONObj schema = fromjson("{type: 'string'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, BSON(AlwaysFalseMatchExpression::kName << 1)); -} - -TEST(JSONSchemaParserTest, TypeNumberTranslatesCorrectly) { - BSONObj schema = fromjson("{properties: {num: {type: 'number'}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, - fromjson("{$or: [{num: {$not: {$exists: true }}}, {num: " - "{ $_internalSchemaType: [ 'number' ]}}]}")); -} - -TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeNumber) { - BSONObj schema = fromjson("{properties: {num: {type: 'number', maximum: 0}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, - fromjson("{$or: [{num: {$not: {$exists: true}}}, {$and: [{num: {$lte: " - "0}}, {num: {$_internalSchemaType: ['number']}}]}]}")); -} - -TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithBsonTypeLong) { - BSONObj schema = - fromjson("{properties: {num: {bsonType: 'long', maximum: 0}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, - fromjson("{$or: [{num: {$not: {$exists: true}}}, {$and: [{num: {$lte: " - "0}}, {num: {$_internalSchemaType: [18]}}]}]}")); -} - -TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithTypeString) { - BSONObj schema = fromjson("{properties: {num: {type: 'string', maximum: 0}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO( - optimizedResult, - fromjson("{$or: [{num: {$not: {$exists: true }}}, {num: {$_internalSchemaType: [2]}}]}")); -} - -TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithNoType) { - BSONObj schema = fromjson("{properties: {num: {maximum: 0}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{num: {$not: {$exists : true}}}, - {num: {$not: {$_internalSchemaType: ["number"]}}}, - {num: {$lte: 0}}] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMaximumIsNotANumber) { - BSONObj schema = fromjson("{maximum: 'foo'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMaxLengthIsNotANumber) { - BSONObj schema = fromjson("{maxLength: 'foo'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMaxLengthIsLessThanZero) { - BSONObj schema = fromjson("{maxLength: -1}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithTypeNumber) { - BSONObj schema = fromjson("{properties: {num: {type: 'number', minimum: 0}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{num: {$not: {$exists : true}}}, - {$and: [ {num: {$gte: 0}}, {num: {$_internalSchemaType: ["number"]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMaxLengthIsNonIntegralDouble) { - BSONObj schema = - fromjson("{properties: {foo: {type: 'string', maxLength: 5.5}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, MaxLengthTranslatesCorrectlyWithIntegralDouble) { - BSONObj schema = - fromjson("{properties: {foo: {type: 'string', maxLength: 5.0}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{foo: {$not: {$exists: true}}}, - {$and: [ {foo: {$_internalSchemaMaxLength: 5}}, {foo: {$_internalSchemaType: [2]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, MaxLengthTranslatesCorrectlyWithTypeString) { - BSONObj schema = - fromjson("{properties: {foo: {type: 'string', maxLength: 5}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{foo: {$not: {$exists : true}}}, - {$and: [ {foo: {$_internalSchemaMaxLength: 5}}, {foo: {$_internalSchemaType: [2]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithBsonTypeLong) { - BSONObj schema = - fromjson("{properties: {num: {bsonType: 'long', minimum: 0}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{num: {$not: {$exists: true}}}, - {$and: [ {num: {$gte: 0}}, {num: {$_internalSchemaType: [18]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithTypeString) { - BSONObj schema = fromjson("{properties: {num: {type: 'string', minimum: 0}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{num: {$not: {$exists: true}}}, {num: {$_internalSchemaType: [2]}}] - })")); -} - - -TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithNoType) { - BSONObj schema = fromjson("{properties: {num: {minimum: 0}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{num: {$not: {$exists: true}}}, - {num: {$not: {$_internalSchemaType: ["number"]}}}, - {num: {$gte: 0}}] - })")); -} - -TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithExclusiveMaximumTrue) { - BSONObj schema = fromjson( - "{properties: {num: {bsonType: 'long', maximum: 0, exclusiveMaximum: true}}," - "type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{num: {$not: {$exists: true}}}, - {$and: [ {num: {$lt: 0}}, {num: {$_internalSchemaType: [18]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, MaximumTranslatesCorrectlyWithExclusiveMaximumFalse) { - BSONObj schema = fromjson( - "{properties: {num: {bsonType: 'long', maximum: 0, exclusiveMaximum: false}}," - "type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{num: {$not: {$exists: true}}}, - {$and: [ {num: {$lte: 0}}, {num: {$_internalSchemaType: [18]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfExclusiveMaximumIsPresentButMaximumIsNot) { - BSONObj schema = fromjson("{exclusiveMaximum: true}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfExclusiveMaximumIsNotABoolean) { - BSONObj schema = fromjson("{maximum: 5, exclusiveMaximum: 'foo'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithExclusiveMinimumTrue) { - BSONObj schema = fromjson( - "{properties: {num: {bsonType: 'long', minimum: 0, exclusiveMinimum: true}}," - "type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{num: {$not: {$exists: true}}}, - {$and: [ {num: {$gt: 0}}, {num: {$_internalSchemaType: [18]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, MinimumTranslatesCorrectlyWithExclusiveMinimumFalse) { - BSONObj schema = fromjson( - "{properties: {num: {bsonType: 'long', minimum: 0, exclusiveMinimum: false}}," - "type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{num: {$not: {$exists: true}}}, - {$and: [ {num: {$gte: 0}}, {num: {$_internalSchemaType: [18]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfExclusiveMinimumIsPresentButMinimumIsNot) { - BSONObj schema = fromjson("{exclusiveMinimum: true}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfExclusiveMinimumIsNotABoolean) { - BSONObj schema = fromjson("{minimum: 5, exclusiveMinimum: 'foo'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMinLengthIsNotANumber) { - BSONObj schema = fromjson("{minLength: 'foo'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMinLengthIsLessThanZero) { - BSONObj schema = fromjson("{minLength: -1}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMinLengthIsNonIntegralDouble) { - BSONObj schema = - fromjson("{properties: {foo: {type: 'string', minLength: 5.5}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, MinLengthTranslatesCorrectlyWithTypeString) { - BSONObj schema = - fromjson("{properties: {foo: {type: 'string', minLength: 5}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{foo: {$not: {$exists: true}}}, - {$and: [ {foo: {$_internalSchemaMinLength: 5}}, {foo: {$_internalSchemaType: [2]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, MinLengthTranslatesCorrectlyWithIntegralDouble) { - BSONObj schema = - fromjson("{properties: {foo: {type: 'string', minLength: 5.0}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{foo: {$not: {$exists: true}}}, - {$and: [ {foo: {$_internalSchemaMinLength: 5}}, {foo: {$_internalSchemaType: [2]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMinimumIsNotANumber) { - BSONObj schema = fromjson("{minimum: 'foo'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseIfPatternIsNotString) { - BSONObj schema = fromjson("{pattern: 6}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, PatternTranslatesCorrectlyWithString) { - BSONObj schema = - fromjson("{properties: {foo: {type: 'string', pattern: 'abc'}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - auto expected = - BSON("$or" << BSON_ARRAY( - BSON("foo" << BSON("$not" << BSON("$exists" << true))) - << BSON("$and" << BSON_ARRAY( - BSON("foo" << BSON("$regex" - << "abc")) - << BSON("foo" << BSON("$_internalSchemaType" << BSON_ARRAY(2))))))); - ASSERT_SERIALIZES_TO(optimizedResult, expected); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMultipleOfIsNotANumber) { - BSONObj schema = fromjson("{multipleOf: 'foo'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMultipleOfIsLessThanZero) { - BSONObj schema = fromjson("{multipleOf: -1}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMultipleOfIsZero) { - BSONObj schema = fromjson("{multipleOf: 0}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, MultipleOfTranslatesCorrectlyWithTypeNumber) { - BSONObj schema = fromjson( - "{properties: {foo: {type: 'number', multipleOf: NumberDecimal('5.3')}}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{foo: {$not: {$exists: true}}}, { - $and: [ - {foo: {$_internalSchemaFmod: [ NumberDecimal('5.3'), 0]}}, - {foo: {$_internalSchemaType: ["number"]}} - ] - }] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfAllOfIsNotAnArray) { - BSONObj schema = fromjson("{properties: {foo: {allOf: 'foo'}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseAllOfIfArrayContainsInvalidSchema) { - BSONObj schema = fromjson("{properties: {foo: {allOf: [{type: {}}]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseAllOfIfArrayIsEmpty) { - BSONObj schema = fromjson("{properties: {foo: {allOf: []}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::BadValue); -} - -TEST(JSONSchemaParserTest, AllOfTranslatesCorrectly) { - BSONObj schema = fromjson("{properties: {foo: {allOf: [{minimum: 0}, {maximum: 10}]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - auto expectedResult = fromjson( - R"({ - $or: - [{foo: {$not: {$exists: true}}}, - { - $and : [ - {$or: [ {foo: {$not: {$_internalSchemaType: ["number"]}}}, {foo: {$gte: 0}}]}, - {$or: [ {foo: {$not: {$_internalSchemaType: ["number"]}}}, {foo: {$lte: 10}}]} - ] - }] - })"); - ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); -} - -TEST(JSONSchemaParserTest, TopLevelAllOfTranslatesCorrectly) { - BSONObj schema = fromjson("{allOf: [{properties: {foo: {type: 'string'}}}]}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{foo: {$not: {$exists: true}}}, {foo: {$_internalSchemaType: [2]}}] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfAnyOfIsNotAnArray) { - BSONObj schema = fromjson("{properties: {foo: {anyOf: 'foo'}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseAnyOfIfArrayContainsInvalidSchema) { - BSONObj schema = fromjson("{properties: {foo: {anyOf: [{type: {}}]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseAnyOfIfArrayIsEmpty) { - BSONObj schema = fromjson("{properties: {foo: {anyOf: []}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::BadValue); -} - -TEST(JSONSchemaParserTest, AnyOfTranslatesCorrectly) { - BSONObj schema = fromjson("{properties: {foo: {anyOf: [{type: 'number'}, {type: 'string'}]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{foo: {$not: {$exists: true}}}, - {foo: {$_internalSchemaType: ["number"]}}, - {foo: {$_internalSchemaType: [2]}}] - })")); -} - -TEST(JSONSchemaParserTest, TopLevelAnyOfTranslatesCorrectly) { - BSONObj schema = fromjson("{anyOf: [{properties: {foo: {type: 'string'}}}]}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{foo: {$not: {$exists: true}}}, {foo: {$_internalSchemaType: [2]}}] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfOneOfIsNotAnArray) { - BSONObj schema = fromjson("{properties: {foo: {oneOf: 'foo'}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseOneOfIfArrayContainsInvalidSchema) { - BSONObj schema = fromjson("{properties: {foo: {oneOf: [{type: {}}]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseOneOfIfArrayIsEmpty) { - BSONObj schema = fromjson("{properties: {foo: {oneOf: []}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::BadValue); -} - -TEST(JSONSchemaParserTest, OneOfTranslatesCorrectly) { - BSONObj schema = fromjson("{properties: {foo: {oneOf: [{minimum: 0}, {maximum: 10}]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{foo: {$not: {$exists: true}}}, - { - $_internalSchemaXor : [ - {$or: [ {foo: {$not: {$_internalSchemaType: ["number"]}}}, {foo: {$gte : 0}}]}, - {$or: [ {foo: {$not: {$_internalSchemaType: ["number"]}}}, {foo: {$lte : 10}}]} - ] - }] - })")); -} - -TEST(JSONSchemaParserTest, TopLevelOneOfTranslatesCorrectly) { - BSONObj schema = fromjson("{oneOf: [{properties: {foo: {type: 'string'}}}]}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{foo: {$not: {$exists: true}}}, {foo: {$_internalSchemaType: [2]}}] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfNotIsNotAnObject) { - BSONObj schema = fromjson("{properties: {foo: {not: 'foo'}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseNotIfObjectContainsInvalidSchema) { - BSONObj schema = fromjson("{properties: {foo: {not: {type: {}}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, NotTranslatesCorrectly) { - BSONObj schema = fromjson("{properties: {foo: {not: {type: 'number'}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{foo: {$not: {$exists: true}}}, {foo: {$not: {$_internalSchemaType: ['number']}}}] - })")); -} - -TEST(JSONSchemaParserTest, TopLevelNotTranslatesCorrectly) { - BSONObj schema = fromjson("{not: {properties: {foo: {type: 'string'}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $nor: - [{$or: [ {foo: {$not: {$exists: true}}}, {foo: {$_internalSchemaType: [2]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMinItemsIsNotANumber) { - auto schema = BSON("minItems" << BSON_ARRAY(1)); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMinItemsIsNotANonNegativeInteger) { - auto schema = BSON("minItems" << -1); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::FailedToParse); - - schema = BSON("minItems" << 3.14); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, MinItemsTranslatesCorrectlyWithNoType) { - auto schema = BSON("minItems" << 1); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); - - schema = fromjson("{properties: {a: {minItems: 1}}}"); - result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{a: {$not: {$exists: true}}}, - {a: {$not: {$_internalSchemaType: [4]}}}, - {a: {$_internalSchemaMinItems: 1}}] - })")); -} - -TEST(JSONSchemaParserTest, MinItemsTranslatesCorrectlyWithArrayType) { - auto schema = fromjson("{properties: {a: {minItems: 1, type: 'array'}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{a: {$not: {$exists: true}}}, - {$and: [ {a: {$_internalSchemaMinItems: 1}}, {a: {$_internalSchemaType: [4]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, MinItemsTranslatesCorrectlyWithNonArrayType) { - auto schema = fromjson("{properties: {a: {minItems: 1, type: 'number'}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{a: {$not: {$exists: true}}}, {a: {$_internalSchemaType: ["number"]}}] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMaxItemsIsNotANumber) { - auto schema = BSON("maxItems" << BSON_ARRAY(1)); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfMaxItemsIsNotANonNegativeInteger) { - auto schema = BSON("maxItems" << -1); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::FailedToParse); - - schema = BSON("maxItems" << 1.60217); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, MaxItemsTranslatesCorrectlyWithNoType) { - auto schema = BSON("maxItems" << 1); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); - - schema = fromjson("{properties: {a: {maxItems: 1}}}"); - result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{a: {$not: {$exists: true}}}, - {a: {$not: {$_internalSchemaType: [4]}}}, - {a: {$_internalSchemaMaxItems: 1}}] - })")); -} - -TEST(JSONSchemaParserTest, MaxItemsTranslatesCorrectlyWithArrayType) { - auto schema = fromjson("{properties: {a: {maxItems: 1, type: 'array'}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{a: {$not: {$exists: true}}}, - {$and: [ {a: {$_internalSchemaMaxItems: 1}}, {a: {$_internalSchemaType: [4]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, MaxItemsTranslatesCorrectlyWithNonArrayType) { - auto schema = fromjson("{properties: {a: {maxItems: 1, type: 'string'}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{a: {$not: {$exists : true}}}, {a: {$_internalSchemaType: [2]}}] - })")); -} - -TEST(JSONSchemaParserTest, RequiredFailsToParseIfNotAnArray) { - BSONObj schema = fromjson("{required: 'field'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, RequiredFailsToParseArrayIsEmpty) { - BSONObj schema = fromjson("{required: []}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, RequiredFailsToParseIfArrayContainsNonString) { - BSONObj schema = fromjson("{required: ['foo', 1]}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, RequiredFailsToParseIfArrayContainsDuplicates) { - BSONObj schema = fromjson("{required: ['foo', 'bar', 'foo']}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, TopLevelRequiredTranslatesCorrectly) { - BSONObj schema = fromjson("{required: ['foo', 'bar']}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, - fromjson("{$and: [{bar: {$exists: true}}, {foo: {$exists: true}}]}")); -} - -TEST(JSONSchemaParserTest, TopLevelRequiredTranslatesCorrectlyWithProperties) { - BSONObj schema = fromjson("{required: ['foo'], properties: {foo: {type: 'number'}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $and: [ - {foo: {$_internalSchemaType: ['number']}}, - {foo: {$exists: true}} - ] - })")); -} - -TEST(JSONSchemaParserTest, RequiredTranslatesCorrectlyWithMultipleElements) { - BSONObj schema = fromjson("{properties: {x: {required: ['y', 'z']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{x: {$not: {$exists: true}}}, {x: {$not: {$_internalSchemaType: [3]}}}, { - x: { - $_internalSchemaObjectMatch: - {$and: [ {y: {$exists: true}}, {z: {$exists: true}}]} - } - }] - })")); -} - -TEST(JSONSchemaParserTest, RequiredTranslatesCorrectlyInsideProperties) { - BSONObj schema = fromjson("{properties: {x: {required: ['y']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{x: {$not: {$exists: true}}}, - {x: {$not: {$_internalSchemaType: [3]}}}, - {x: {$_internalSchemaObjectMatch: {y: {$exists: true}}}}] - })")); -} - -TEST(JSONSchemaParserTest, RequiredTranslatesCorrectlyInsidePropertiesWithSiblingProperties) { - BSONObj schema = - fromjson("{properties: {x:{required: ['y'], properties: {y: {type: 'number'}}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - auto expectedResult = fromjson( - R"({ - $or: - [{x: {$not: {$exists: true}}}, - { - $and: [ - { - $or: [ - {x: {$not: {$_internalSchemaType: [3]}}}, - {x: {$_internalSchemaObjectMatch: {y : {$_internalSchemaType: ["number"]}}}} - ] - }, - { - $or: [ - {x: {$not: {$_internalSchemaType: [3]}}}, - {x: {$_internalSchemaObjectMatch: {y: {$exists: true}}}} - ] - } - ] - }] - })"); - ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); -} - -TEST(JSONSchemaParserTest, SharedJsonAndBsonTypeAliasesTranslateIdentically) { - for (auto&& mapEntry : MatcherTypeSet::kJsonSchemaTypeAliasMap) { - auto typeAlias = mapEntry.first; - // JSON Schema spells its bool type as "boolean", whereas MongoDB calls it "bool". - auto bsonTypeAlias = - (typeAlias == JSONSchemaParser::kSchemaTypeBoolean) ? "bool" : typeAlias; - - BSONObj typeSchema = BSON("properties" << BSON("f" << BSON("type" << typeAlias))); - BSONObj bsonTypeSchema = - BSON("properties" << BSON("f" << BSON("bsonType" << bsonTypeAlias))); - auto typeResult = JSONSchemaParser::parse(new ExpressionContextForTest(), typeSchema); - ASSERT_OK(typeResult.getStatus()); - auto bsonTypeResult = - JSONSchemaParser::parse(new ExpressionContextForTest(), bsonTypeSchema); - ASSERT_OK(bsonTypeResult.getStatus()); - - BSONObjBuilder typeBuilder; - MatchExpression::optimize(std::move(typeResult.getValue()))->serialize(&typeBuilder); - - BSONObjBuilder bsonTypeBuilder; - MatchExpression::optimize(std::move(bsonTypeResult.getValue())) - ->serialize(&bsonTypeBuilder); - - ASSERT_BSONOBJ_EQ(typeBuilder.obj(), bsonTypeBuilder.obj()); - } -} - -TEST(JSONSchemaParserTest, MinPropertiesFailsToParseIfNotNumber) { - BSONObj schema = fromjson("{minProperties: null}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, MaxPropertiesFailsToParseIfNotNumber) { - BSONObj schema = fromjson("{maxProperties: null}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, MinPropertiesFailsToParseIfNegative) { - BSONObj schema = fromjson("{minProperties: -2}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, MaxPropertiesFailsToParseIfNegative) { - BSONObj schema = fromjson("{maxProperties: -2}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, MinPropertiesFailsToParseIfNotAnInteger) { - BSONObj schema = fromjson("{minProperties: 1.1}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, MaxPropertiesFailsToParseIfNotAnInteger) { - BSONObj schema = fromjson("{maxProperties: 1.1}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, TopLevelMinPropertiesTranslatesCorrectly) { - BSONObj schema = fromjson("{minProperties: 0}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{$_internalSchemaMinProperties: 0}")); -} - -TEST(JSONSchemaParserTest, TopLevelMaxPropertiesTranslatesCorrectly) { - BSONObj schema = fromjson("{maxProperties: 0}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{$_internalSchemaMaxProperties: 0}")); -} - -TEST(JSONSchemaParserTest, NestedMinPropertiesTranslatesCorrectly) { - BSONObj schema = - fromjson("{properties: {obj: {type: 'object', minProperties: 2}}, required: ['obj']}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - auto expectedResult = fromjson( - R"({ - $and: [ - {obj: {$exists: true}}, - {obj: {$_internalSchemaObjectMatch: {$_internalSchemaMinProperties: 2}}}, - {obj: {$_internalSchemaType: [3]}} - ] - })"); - ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); -} - -TEST(JSONSchemaParserTest, NestedMaxPropertiesTranslatesCorrectly) { - BSONObj schema = - fromjson("{properties: {obj: {type: 'object', maxProperties: 2}}, required: ['obj']}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - auto expectedResult = fromjson( - R"({ - $and: [ - {obj: {$exists: true}}, - {obj: {$_internalSchemaObjectMatch: {$_internalSchemaMaxProperties: 2}}}, - {obj: {$_internalSchemaType: [3]}} - ] - })"); - ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); -} - -TEST(JSONSchemaParserTest, NestedMinPropertiesTranslatesCorrectlyWithoutRequired) { - BSONObj schema = fromjson("{properties: {obj: {type: 'object', minProperties: 2}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - auto expectedResult = fromjson(R"( - { - $or: - [{obj: {$not: {$exists: true}}}, { - $and: [ - {obj: {$_internalSchemaObjectMatch: {$_internalSchemaMinProperties: 2}}}, - {obj: {$_internalSchemaType: [3]}} - ] - }] - })"); - ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); -} - -TEST(JSONSchemaParserTest, NestedMaxPropertiesTranslatesCorrectlyWithoutRequired) { - BSONObj schema = fromjson("{properties: {obj: {type: 'object', maxProperties: 2}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - auto expectedResult = fromjson(R"( - { - $or: - [{obj: {$not: {$exists: true}}}, { - $and: [ - {obj: {$_internalSchemaObjectMatch: {$_internalSchemaMaxProperties: 2}}}, - {obj: {$_internalSchemaType: [3]}} - ] - }] - })"); - ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); -} - -TEST(JSONSchemaParserTest, FailsToParseIfTypeArrayHasRepeatedAlias) { - BSONObj schema = fromjson("{properties: {obj: {type: ['object', 'string', 'object']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, FailsToParseIfBsonTypeArrayHasRepeatedAlias) { - BSONObj schema = fromjson("{properties: {obj: {bsonType: ['object', 'string', 'object']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, FailsToParseIfTypeArrayIsEmpty) { - BSONObj schema = fromjson("{properties: {obj: {type: []}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, FailsToParseIfBsonTypeArrayIsEmpty) { - BSONObj schema = fromjson("{properties: {obj: {bsonType: []}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, FailsToParseIfTypeArrayContainsNonString) { - BSONObj schema = fromjson("{properties: {obj: {type: [1]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, FailsToParseIfBsonTypeArrayContainsNonString) { - BSONObj schema = fromjson("{properties: {obj: {bsonType: [1]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, FailsToParseIfTypeArrayContainsUnknownAlias) { - BSONObj schema = fromjson("{properties: {obj: {type: ['objectId']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, FailsToParseNicelyIfTypeArrayContainsKnownUnsupportedAlias) { - BSONObj schema = fromjson("{properties: {obj: {type: ['number', 'integer']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema type 'integer' is not currently supported"); -} - -TEST(JSONSchemaParserTest, FailsToParseIfBsonTypeArrayContainsUnknownAlias) { - BSONObj schema = fromjson("{properties: {obj: {bsonType: ['unknown']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, CanTranslateTopLevelTypeArrayWithoutObject) { - BSONObj schema = fromjson("{type: ['number', 'string']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_SERIALIZES_TO(result.getValue(), BSON(AlwaysFalseMatchExpression::kName << 1)); -} - -TEST(JSONSchemaParserTest, CanTranslateTopLevelBsonTypeArrayWithoutObject) { - BSONObj schema = fromjson("{bsonType: ['number', 'string']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_SERIALIZES_TO(result.getValue(), BSON(AlwaysFalseMatchExpression::kName << 1)); -} - -TEST(JSONSchemaParserTest, CanTranslateTopLevelTypeArrayWithObject) { - BSONObj schema = fromjson("{type: ['number', 'object']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_SERIALIZES_TO(result.getValue(), fromjson("{}")); -} - -TEST(JSONSchemaParserTest, CanTranslateTopLevelBsonTypeArrayWithObject) { - BSONObj schema = fromjson("{bsonType: ['number', 'object']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_SERIALIZES_TO(result.getValue(), fromjson("{}")); -} - -TEST(JSONSchemaParserTest, CanTranslateNestedTypeArray) { - BSONObj schema = fromjson("{properties: {a: {type: ['number', 'object']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{a: {$not: {$exists: true}}}, {a: {$_internalSchemaType: [ "number", 3 ]}}] - })")); -} - -TEST(JSONSchemaParserTest, CanTranslateNestedBsonTypeArray) { - BSONObj schema = fromjson("{properties: {a: {bsonType: ['number', 'objectId']}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{a: {$not: {$exists: true}}}, {a: {$_internalSchemaType: [ "number", 7 ]}}] - })")); -} - -TEST(JSONSchemaParserTest, DependenciesFailsToParseIfNotAnObject) { - BSONObj schema = fromjson("{dependencies: []}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, DependenciesFailsToParseIfDependencyIsNotObjectOrArray) { - BSONObj schema = fromjson("{dependencies: {a: ['b'], bad: 1}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, DependenciesFailsToParseIfNestedSchemaIsInvalid) { - BSONObj schema = fromjson("{dependencies: {a: {invalid: 1}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, PropertyDependencyFailsToParseIfEmptyArray) { - BSONObj schema = fromjson("{dependencies: {a: []}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, PropertyDependencyFailsToParseIfArrayContainsNonStringElement) { - BSONObj schema = fromjson("{dependencies: {a: ['b', 1]}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, PropertyDependencyFailsToParseIfRepeatedArrayElement) { - BSONObj schema = fromjson("{dependencies: {a: ['b', 'b']}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, TopLevelSchemaDependencyTranslatesCorrectly) { - BSONObj schema = fromjson("{dependencies: {a: {properties: {b: {type: 'string'}}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $_internalSchemaCond: - [{a: {$exists: true}}, - {$or: [ {b: {$not: {$exists: true}}}, {b: {$_internalSchemaType: [2]}}]}, - {$alwaysTrue: 1}] - })")); -} - -TEST(JSONSchemaParserTest, TopLevelPropertyDependencyTranslatesCorrectly) { - BSONObj schema = fromjson("{dependencies: {a: ['b', 'c']}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $_internalSchemaCond: [ - {a: {$exists: true}}, - { - $and: [ - {b: {$exists: true}}, - {c: {$exists: true}} - ] - }, - {$alwaysTrue: 1} - ] - })")); -} - -TEST(JSONSchemaParserTest, NestedSchemaDependencyTranslatesCorrectly) { - BSONObj schema = - fromjson("{properties: {a: {dependencies: {b: {properties: {c: {type: 'object'}}}}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{a: {$not: {$exists: true}}}, { - $_internalSchemaCond: [ - {a: {$_internalSchemaObjectMatch : {b: {$exists: true}}}}, - { - $or : [ - {a: {$not: {$_internalSchemaType: [3]}}}, - { - a: { - $_internalSchemaObjectMatch : { - $or : [ - {c: {$not: {$exists: true}}}, - {c: {$_internalSchemaType: [3]}} - ] - } - } - } - ] - }, - {$alwaysTrue : 1} - ] - }] - })")); -} - -TEST(JSONSchemaParserTest, NestedPropertyDependencyTranslatesCorrectly) { - BSONObj schema = fromjson("{properties: {a: {dependencies: {b: ['c', 'd']}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - auto expectedResult = fromjson(R"( - { - $or: - [{a: {$not: {$exists: true}}}, { - $_internalSchemaCond: [ - {a: {$_internalSchemaObjectMatch : {b : {$exists : true}}}}, - { - $and: [ - {a: {$_internalSchemaObjectMatch: {c: {$exists: true}}}}, - {a: {$_internalSchemaObjectMatch: {d: {$exists: true}}}} - ] - }, - {$alwaysTrue: 1} - ] - }] - })"); - ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); -} - -TEST(JSONSchemaParserTest, EmptyDependenciesTranslatesCorrectly) { - BSONObj schema = fromjson("{dependencies: {}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_SERIALIZES_TO(result.getValue(), fromjson("{$and: [{}]}")); -} - -TEST(JSONSchemaParserTest, UnsupportedKeywordsFailNicely) { - auto result = - JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{default: {}}")); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword 'default' is not currently supported"); - - result = JSONSchemaParser::parse(new ExpressionContextForTest(), - fromjson("{definitions: {numberField: {type: 'number'}}}")); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword 'definitions' is not currently supported"); - - result = JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{format: 'email'}")); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword 'format' is not currently supported"); - - result = JSONSchemaParser::parse(new ExpressionContextForTest(), - fromjson("{id: 'someschema.json'}")); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword 'id' is not currently supported"); - - result = JSONSchemaParser::parse(new ExpressionContextForTest(), - BSON("$ref" - << "#/definitions/positiveInt")); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword '$ref' is not currently supported"); - - result = JSONSchemaParser::parse(new ExpressionContextForTest(), - fromjson("{$schema: 'hyper-schema'}")); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword '$schema' is not currently supported"); - - result = - JSONSchemaParser::parse(new ExpressionContextForTest(), - fromjson("{$schema: 'http://json-schema.org/draft-04/schema#'}")); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword '$schema' is not currently supported"); -} - -TEST(JSONSchemaParserTest, FailsToParseIfDescriptionIsNotAString) { - auto result = - JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{description: {}}")); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, CorrectlyParsesDescriptionAsString) { - auto result = - JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{description: 'str'}")); - ASSERT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, CorrectlyParsesNestedDescriptionAsString) { - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), - fromjson("{properties: {a: {description: 'str'}}}")); - ASSERT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, FailsToParseIfTitleIsNotAString) { - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{title: {}}")); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, CorrectlyParsesTitleAsString) { - auto result = - JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{title: 'str'}")); - ASSERT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, CorrectlyParsesNestedTitleAsString) { - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), - fromjson("{properties: {a: {title: 'str'}}}")); - ASSERT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, PatternPropertiesFailsToParseIfNotObject) { - BSONObj schema = fromjson("{patternProperties: 1}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, PatternPropertiesFailsToParseIfOnePropertyIsNotObject) { - BSONObj schema = fromjson("{patternProperties: {a: {}, b: 1}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, PatternPropertiesFailsToParseIfNestedSchemaIsInvalid) { - BSONObj schema = fromjson("{patternProperties: {a: {invalid: 1}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, PatternPropertiesFailsToParseIfPropertyNameIsAnInvalidRegex) { - BSONObj schema = fromjson("{patternProperties: {'[': {}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, AdditionalPropertiesFailsToParseIfNotBoolOrString) { - BSONObj schema = fromjson("{additionalProperties: 1}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, AdditionalPropertiesFailsToParseIfNestedSchemaIsInvalid) { - BSONObj schema = fromjson("{additionalProperties: {invalid: 1}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_NOT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, TopLevelPatternPropertiesTranslatesCorrectly) { - BSONObj schema = - fromjson("{patternProperties: {'^a': {type: 'number'}, '^b': {type: 'string'}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $_internalSchemaAllowedProperties: { - properties: [], - namePlaceholder: "i", - patternProperties: [ - { - regex: /^a/, - expression: { - i: {$_internalSchemaType: ['number']} - } - }, - { - regex: /^b/, - expression: { - i: {$_internalSchemaType: [2]} - } - } - ], - otherwise: {$alwaysTrue: 1} - } - })")); -} - -TEST(JSONSchemaParserTest, TopLevelAdditionalPropertiesFalseTranslatesCorrectly) { - BSONObj schema = fromjson("{additionalProperties: false}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $_internalSchemaAllowedProperties: { - properties: [], - namePlaceholder: "i", - patternProperties: [], - otherwise: {$alwaysFalse: 1} - } - })")); -} - -TEST(JSONSchemaParserTest, TopLevelAdditionalPropertiesTrueTranslatesCorrectly) { - BSONObj schema = fromjson("{additionalProperties: true}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $_internalSchemaAllowedProperties: { - properties: [], - namePlaceholder: "i", - patternProperties: [], - otherwise: {$alwaysTrue: 1} - } - })")); -} - -TEST(JSONSchemaParserTest, TopLevelAdditionalPropertiesTypeNumberTranslatesCorrectly) { - BSONObj schema = fromjson("{additionalProperties: {type: 'number'}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $_internalSchemaAllowedProperties: { - properties: [], - namePlaceholder: "i", - patternProperties: [], - otherwise: {i: {$_internalSchemaType: ['number']}} - } - })")); -} - -TEST(JSONSchemaParserTest, NestedAdditionalPropertiesTranslatesCorrectly) { - BSONObj schema = fromjson("{properties: {obj: {additionalProperties: {type: 'number'}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{obj: {$not: {$exists: true}}}, {obj: {$not: {$_internalSchemaType: [3]}}}, { - obj: { - $_internalSchemaObjectMatch: { - $_internalSchemaAllowedProperties: { - properties: [], - namePlaceholder: "i", - patternProperties: [], - otherwise: {i: {$_internalSchemaType: ["number"]}} - } - } - } - }] - })")); -} - -TEST(JSONSchemaParserTest, - PropertiesPatternPropertiesAndAdditionalPropertiesTranslateCorrectlyTogether) { - BSONObj schema = fromjson( - "{properties: {a: {}, b: {}}, patternProperties: {'^c': {}}, additionalProperties: false}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $_internalSchemaAllowedProperties: { - properties: ["a", "b"], - namePlaceholder: "i", - patternProperties: [ - {regex: /^c/, expression: {}} - ], - otherwise: {$alwaysFalse: 1} - } - })")); -} - -TEST(JSONSchemaParserTest, - PropertiesPatternPropertiesAdditionalPropertiesAndRequiredTranslateCorrectlyTogether) { - BSONObj schema = fromjson( - "{properties: {a: {}, b: {}}, required: ['a'], patternProperties: {'^c': {}}, " - "additionalProperties: false}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $and: [ - { - $_internalSchemaAllowedProperties: { - properties: ["a", "b"], - namePlaceholder: "i", - patternProperties: [ - {regex: /^c/, expression: {}} - ], - otherwise: {$alwaysFalse: 1} - } - }, - {a: {$exists: true}} - ] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfUniqueItemsIsNotABoolean) { - auto schema = BSON("uniqueItems" << 1); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, UniqueItemsFalseGeneratesAlwaysTrueExpression) { - auto schema = fromjson("{properties: {a: {uniqueItems: false}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); -} - -TEST(JSONSchemaParserTest, UniqueItemsTranslatesCorrectlyWithNoType) { - auto schema = BSON("uniqueItems" << true); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); - - schema = fromjson("{properties: {a: {uniqueItems: true}}}"); - result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{a: {$not: {$exists: true}}}, - {a: {$not: {$_internalSchemaType: [4]}}}, - {a: {$_internalSchemaUniqueItems: true}}] - })")); -} - -TEST(JSONSchemaParserTest, UniqueItemsTranslatesCorrectlyWithTypeArray) { - auto schema = fromjson("{properties: {a: {type: 'array', uniqueItems: true}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{a: {$not: {$exists: true}}}, - {$and: [ {a: {$_internalSchemaUniqueItems: true}}, {a: {$_internalSchemaType: [4]}}]}] - })")); -} - -TEST(JSONSchemaParserTest, CorrectlyIgnoresUnknownKeywordsParameterIsSet) { - const auto ignoreUnknownKeywords = true; - - auto schema = fromjson("{ignored_keyword: 1}"); - ASSERT_OK(JSONSchemaParser::parse(new ExpressionContextForTest(), schema, ignoreUnknownKeywords) - .getStatus()); - - schema = fromjson("{properties: {a: {ignored_keyword: 1}}}"); - ASSERT_OK(JSONSchemaParser::parse(new ExpressionContextForTest(), schema, ignoreUnknownKeywords) - .getStatus()); - - schema = fromjson("{properties: {a: {oneOf: [{ignored_keyword: {}}]}}}"); - ASSERT_OK(JSONSchemaParser::parse(new ExpressionContextForTest(), schema, ignoreUnknownKeywords) - .getStatus()); -} - -TEST(JSONSchemaParserTest, FailsToParseUnsupportedKeywordsWhenIgnoreUnknownParameterIsSet) { - const auto ignoreUnknownKeywords = true; - - auto result = JSONSchemaParser::parse( - new ExpressionContextForTest(), fromjson("{default: {}}"), ignoreUnknownKeywords); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword 'default' is not currently supported"); - - result = JSONSchemaParser::parse(new ExpressionContextForTest(), - fromjson("{definitions: {numberField: {type: 'number'}}}"), - ignoreUnknownKeywords); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword 'definitions' is not currently supported"); - - result = JSONSchemaParser::parse( - new ExpressionContextForTest(), fromjson("{format: 'email'}"), ignoreUnknownKeywords); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword 'format' is not currently supported"); - - result = JSONSchemaParser::parse( - new ExpressionContextForTest(), fromjson("{id: 'someschema.json'}"), ignoreUnknownKeywords); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword 'id' is not currently supported"); - - result = JSONSchemaParser::parse(new ExpressionContextForTest(), - BSON("$ref" - << "#/definitions/positiveInt"), - ignoreUnknownKeywords); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword '$ref' is not currently supported"); - - result = JSONSchemaParser::parse(new ExpressionContextForTest(), - fromjson("{$schema: 'hyper-schema'}"), - ignoreUnknownKeywords); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword '$schema' is not currently supported"); - - result = - JSONSchemaParser::parse(new ExpressionContextForTest(), - fromjson("{$schema: 'http://json-schema.org/draft-04/schema#'}"), - ignoreUnknownKeywords); - ASSERT_STRING_CONTAINS(result.getStatus().reason(), - "$jsonSchema keyword '$schema' is not currently supported"); -} - -TEST(JSONSchemaParserTest, FailsToParseIfItemsIsNotAnArrayOrObject) { - auto schema = BSON("items" << 1); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseIfItemsIsAnArrayWithANonObject) { - auto schema = fromjson("{items: [{type: 'string'}, 'blah']}"); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseIfItemsIsAnInvalidSchema) { - auto schema = BSON("items" << BSON("invalid" << 1)); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfItemsIsAnArrayThatContainsAnInvalidSchema) { - auto schema = fromjson("{items: [{type: 'string'}, {invalid: 1}]}"); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, ItemsParsesSuccessfullyAsArrayAtTopLevel) { - auto schema = fromjson("{items: [{type: 'string'}]}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); -} - -TEST(JSONSchemaParserTest, ItemsParsesSuccessfullyAsObjectAtTopLevel) { - auto schema = fromjson("{items: {type: 'string'}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); -} - -TEST(JSONSchemaParserTest, ItemsParsesSuccessfullyAsArrayInNestedSchema) { - auto schema = fromjson("{properties: {a: {items: [{maxLength: 4}, {minimum: 0}]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - auto expectedResult = fromjson(R"( - { - $or: - [{a: {$not: {$exists: true}}}, {a: {$not: {$_internalSchemaType: [4]}}}, { - $and: [ - { - a: { - $_internalSchemaMatchArrayIndex: { - index: 0, - namePlaceholder: "i", - expression: { - $or: [ - {i: {$not: {$_internalSchemaType: [2]}}}, - {i: {$_internalSchemaMaxLength: 4}} - ] - } - } - } - }, - { - a: { - $_internalSchemaMatchArrayIndex : { - index: 1, - namePlaceholder: "i", - expression: { - $or: [ - {i : {$not: {$_internalSchemaType: ["number"]}}}, - {i: {$gte: 0}} - ] - } - } - } - } - ] - }] - })"); - ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); -} - -TEST(JSONSchemaParserTest, ItemsParsesSuccessfullyAsObjectInNestedSchema) { - auto schema = fromjson("{properties: {a: {items: {type: 'string'}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{a: {$not: {$exists: true}}}, - {a: {$not: {$_internalSchemaType: [4]}}}, - {a: {$_internalSchemaAllElemMatchFromIndex : [ 0, {i: {$_internalSchemaType: [2]}}]}}] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfAdditionalItemsIsNotAnObjectOrBoolean) { - auto schema = BSON("items" << BSONObj() << "additionalItems" << 1); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::TypeMismatch); - - schema = BSON("additionalItems" << 1); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseIfAdditionalItemsIsAnInvalidSchema) { - auto schema = BSON("items" << BSONObj() << "additionalItems" << BSON("invalid" << 1)); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::FailedToParse); - - schema = BSON("additionalItems" << BSON("invalid" << 1)); - ASSERT_EQ(JSONSchemaParser::parse(new ExpressionContextForTest(), schema).getStatus(), - ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, AdditionalItemsTranslatesSucessfullyAsBooleanAtTopLevel) { - auto schema = fromjson("{items: [], additionalItems: true}"); - auto expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(expr.getStatus()); - auto optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); - ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); - - schema = fromjson("{items: [], additionalItems: false}"); - expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(expr.getStatus()); - optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); - ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); -} - -TEST(JSONSchemaParserTest, AdditionalItemsTranslatesSucessfullyAsObjectAtTopLevel) { - auto schema = fromjson("{items: [], additionalItems: {multipleOf: 7}}"); - auto expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(expr.getStatus()); - auto optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); - ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); -} - -TEST(JSONSchemaParserTest, AdditionalItemsTranslatesSucessfullyAsBooleanInNestedSchema) { - auto schema = fromjson("{properties: {a: {items: [], additionalItems: true}}}"); - auto expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(expr.getStatus()); - auto optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); - auto expectedResult = fromjson(R"( - { - $or: - [{a: {$not: {$exists: true}}}, - {a: {$not: {$_internalSchemaType: [4]}}}, - {a: {$_internalSchemaAllElemMatchFromIndex: [0, {$alwaysTrue: 1}]}}] - })"); - ASSERT_SERIALIZES_TO(optimizedExpr, expectedResult); - - schema = fromjson("{properties: {a: {items: [], additionalItems: false}}}"); - expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(expr.getStatus()); - optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); - expectedResult = fromjson(R"( - { - $or: - [{a: {$not: {$exists: true}}}, - {a: {$not: {$_internalSchemaType: [4]}}}, - {a: {$_internalSchemaAllElemMatchFromIndex: [0, {$alwaysFalse: 1}]}}] - })"); - ASSERT_SERIALIZES_TO(optimizedExpr, expectedResult); -} - -TEST(JSONSchemaParserTest, AdditionalItemsGeneratesEmptyExpressionAtTopLevelIfItemsNotPresent) { - auto schema = BSON("additionalItems" << true); - auto expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(expr.getStatus()); - auto optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); - ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); - - schema = BSON("additionalItems" << false); - expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(expr.getStatus()); - optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); - ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); - - schema = BSON("additionalItems" << BSON("minLength" << 1)); - expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(expr.getStatus()); - optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); - ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); -} - -TEST(JSONSchemaParserTest, AdditionalItemsGeneratesEmptyExpressionInNestedSchemaIfItemsNotPresent) { - auto schema = fromjson("{properties: {foo: {additionalItems: true}}}"); - auto expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(expr.getStatus()); - auto optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); - ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); - - - schema = fromjson("{properties: {foo: {additionalItems: false}}}"); - expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(expr.getStatus()); - optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); - ASSERT_SERIALIZES_TO(optimizedExpr, fromjson("{}")); -} - -TEST(JSONSchemaParserTest, AdditionalItemsGeneratesEmptyExpressionIfItemsAnObject) { - auto schema = fromjson("{properties: {a: {items: {minimum: 7}, additionalItems: false}}}"); - auto expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(expr.getStatus()); - auto optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); - auto expectedResult = fromjson(R"( - { - $or: - [{a: {$not: {$exists: true}}}, - {a: {$not: {$_internalSchemaType: [4]}}}, - { - a: { - $_internalSchemaAllElemMatchFromIndex: [ - 0, - {$or: [ {i: {$not: {$_internalSchemaType: ["number"]}}}, {i: {$gte: 7}}]} - ] - } - }] - })"); - ASSERT_SERIALIZES_TO(optimizedExpr, expectedResult); - - schema = fromjson("{properties: {a: {items: {minimum: 7}, additionalItems: {minLength: 7}}}}"); - expr = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(expr.getStatus()); - optimizedExpr = MatchExpression::optimize(std::move(expr.getValue())); - expectedResult = fromjson(R"( - { - $or: - [{a: {$not: {$exists: true}}}, - {a: {$not: {$_internalSchemaType: [4]}}}, - { - a: { - $_internalSchemaAllElemMatchFromIndex : [ - 0, - {$or: [ {i: {$not: {$_internalSchemaType: ["number"]}}}, {i: {$gte: 7}}]} - ] - } - }] - })"); - ASSERT_SERIALIZES_TO(optimizedExpr, expectedResult); -} - -TEST(JSONSchemaParserTest, FailsToParseIfEnumIsNotAnArray) { - BSONObj schema = fromjson("{properties: {foo: {enum: 'foo'}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseEnumIfArrayIsEmpty) { - BSONObj schema = fromjson("{properties: {foo: {enum: []}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseEnumIfArrayContainsDuplicateValue) { - BSONObj schema = fromjson("{properties: {foo: {enum: [1, 2, 1]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); - - schema = fromjson("{properties: {foo: {enum: [{a: 1, b: 1}, {b: 1, a: 1}]}}}"); - result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, EnumTranslatesCorrectly) { - BSONObj schema = fromjson("{properties: {foo: {enum: [1, '2', [3]]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ - $or: - [{foo: {$not: {$exists: true}}}, - {foo: {$_internalSchemaEq: 1}}, - {foo: {$_internalSchemaEq: "2"}}, - {foo: {$_internalSchemaEq: [3]}}] - })")); -} - -TEST(JSONSchemaParserTest, TopLevelEnumTranslatesCorrectly) { - BSONObj schema = fromjson("{enum: [1, {foo: 1}]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{$_internalSchemaRootDocEq: {foo: 1}}")); -} - -TEST(JSONSchemaParserTest, TopLevelEnumWithZeroObjectsTranslatesCorrectly) { - BSONObj schema = fromjson("{enum: [1, 'impossible', true]}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{$alwaysFalse: 1}")); -} - -TEST(JSONSchemaParserTest, EncryptTranslatesCorrectly) { - BSONObj schema = fromjson("{properties: {foo: {encrypt: {}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"( - { - $or: - [{foo: {$not: {$exists: true}}}, { - $and: - [ {foo: {$_internalSchemaBinDataSubType: 6}}, {foo: {$_internalSchemaType: [5]}} ] - }] - })")); -} - -TEST(JSONSchemaParserTest, EncryptWithSingleBsonTypeTranslatesCorrectly) { - BSONObj schema = fromjson("{properties: {foo: {encrypt: {bsonType: \"string\"}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"( - { - $or: - [{foo: {$not: {$exists: true}}}, { - $and: - [ {foo: {$_internalSchemaBinDataSubType: 6}}, - {foo: {$_internalSchemaBinDataEncryptedType: [2]}}, - {foo: {$_internalSchemaType: [5]}}] - }] - })")); -} - -TEST(JSONSchemaParserTest, EncryptWithArrayOfMultipleTypesTranslatesCorrectly) { - BSONObj schema = fromjson("{properties: {foo: {encrypt: {bsonType: [\"string\",\"date\"]}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"( - { - $or: - [{foo: {$not: {$exists: true}}}, { - $and: - [ {foo: {$_internalSchemaBinDataSubType: 6}}, - {foo: {$_internalSchemaBinDataEncryptedType: [2, 9]}}, - {foo: {$_internalSchemaType: [5]}}] - }] - })")); -} - -TEST(JSONSchemaParserTest, TopLevelEncryptTranslatesCorrectly) { - BSONObj schema = fromjson("{encrypt: {}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, BSON(AlwaysFalseMatchExpression::kName << 1)); -} - -TEST(JSONSchemaParserTest, NestedEncryptTranslatesCorrectly) { - BSONObj schema = - fromjson("{properties: {a: {type: 'object', properties: {b: {encrypt: {}}}}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"( - { - $or: - [{a: {$not: {$exists: true}}}, { - $and: [ - { - a: { - $_internalSchemaObjectMatch: { - $or: [ - {b: {$not : {$exists: true}}}, - { - $and: [ - {b: {$_internalSchemaBinDataSubType: 6}}, - {b: {$_internalSchemaType: [5]}} - ] - } - ] - } - } - }, - {a: {$_internalSchemaType: [3]}} - ] - }] - })")); -} - -TEST(JSONSchemaParserTest, NestedEncryptInArrayTranslatesCorrectly) { - BSONObj schema = fromjson("{properties: {a: {type: 'array', items: {encrypt: {}}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); - auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); - ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"( - { - $or: - [{a: {$not: {$exists: true}}}, { - $and: [ - { - a: { - $_internalSchemaAllElemMatchFromIndex : [ - 0, - { - $and: [ - {i: {$_internalSchemaBinDataSubType: 6}}, - {i: {$_internalSchemaType: [5]}} - ] - } - ] - } - }, - {a: {$_internalSchemaType: [4]}} - ] - }] - })")); -} - -TEST(JSONSchemaParserTest, FailsToParseIfBothEncryptAndTypeArePresent) { - BSONObj schema = fromjson("{encrypt: {}, type: 'object'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfBothEncryptAndBSONTypeArePresent) { - BSONObj schema = fromjson("{encrypt: {}, bsonType: 'binData'}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfEncryptValueIsNotObject) { - BSONObj schema = fromjson("{properties: {foo: {encrypt: 12}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, ParseSucceedsWithEmptyEncryptObject) { - BSONObj schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSONObj()))); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, ParseSucceedsWithBsonType) { - BSONObj schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("bsonType" - << "int")))); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, ParseFailsWithBsonTypeGivenByCode) { - BSONObj schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("bsonType" << 5)))); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus().code(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, ParseSucceedsWithArrayOfBsonTypes) { - BSONObj schema = - BSON("properties" << BSON( - "foo" << BSON("encrypt" << BSON("bsonType" << BSON_ARRAY("int" - << "date" - << "string"))))); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, ParseSucceedsIfEncryptFieldsAreValid) { - auto schema = BSON( - "properties" << BSON( - "foo" << BSON("encrypt" << BSON("bsonType" - << "string" - << "keyId" - << "/pointer" - << "algorithm" - << "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")))); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, FailsToParseIfEncryptHasBadFieldName) { - BSONObj schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("keyIdx" - << "/pointer")))); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus().code(), 40415); - schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("bsonType" - << "bool" - << "keyIdx" - << "/pointer")))); - result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus().code(), 40415); -} - -TEST(JSONSchemaParserTest, FailsToParseWithBadKeyIdArray) { - auto schema = BSON( - "properties" << BSON("foo" << BSON("encrypt" << BSON("keyId" << BSON_ARRAY("nonsense" - << "again"))))); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus().code(), 51088); -} - -TEST(JSONSchemaParserTest, FailsToParseWithBadBSONType) { - auto schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("bsonType" - << "Stringx")))); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus().code(), ErrorCodes::BadValue); - - schema = BSON("properties" << BSON( - "foo" << BSON("encrypt" << BSON("bsonType" << (BSONType::JSTypeMax + 1))))); - result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus().code(), ErrorCodes::TypeMismatch); -} - -TEST(JSONSchemaParserTest, FailsToParseWithBadAlgorithm) { - auto schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("algorithm" - << "Stringx")))); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus().code(), ErrorCodes::BadValue); -} - -TEST(JSONSchemaParserTest, FailsToParseWithBadPointer) { - auto schema = BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("keyId" - << "invalidPointer")))); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus().code(), 51065); -} - -TEST(JSONSchemaParserTest, TopLevelEncryptMetadataValidatedCorrectly) { - BSONObj schema = fromjson( - "{encryptMetadata: {algorithm: \"AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic\"," - " keyId: [{$binary: \"ASNFZ4mrze/ty6mHZUMhAQ==\", $type: \"04\"}]}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, NestedEncryptMetadataValidatedCorrectly) { - BSONObj schema = fromjson( - "{properties: {a: {encryptMetadata: {algorithm: \"AEAD_AES_256_CBC_HMAC_SHA_512-Random\", " - "keyId: [{$binary: \"ASNFZ4mrze/ty6mHZUMhAQ==\", $type: \"04\"}]}}}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_OK(result.getStatus()); -} - -TEST(JSONSchemaParserTest, FailsToParseIfEncryptMetadataValueIsEmptyObject) { - BSONObj schema = fromjson("{encryptMetadata: {}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseIfBothEncryptAndEncryptMetadataAreSiblings) { - BSONObj schema = fromjson( - "{encrypt: {}, encryptMetadata: {algorithm: " - "\"AEAD_AES_256_CBC_HMAC_SHA_512-Random\"}}"); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); -} - -TEST(JSONSchemaParserTest, FailsToParseWithNonUUIDArrayElement) { - BSONArrayBuilder builder; - UUID::gen().appendToArrayBuilder(&builder); - UUID::gen().appendToArrayBuilder(&builder); - builder.appendBinData(16, BinDataType::Encrypt, "16charactershere"); - auto schema = - BSON("properties" << BSON("foo" << BSON("encrypt" << BSON("keyId" << builder.arr())))); - auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); - ASSERT_EQ(result.getStatus().code(), 51084); -} - -} // namespace -} // namespace mongo diff --git a/src/mongo/db/matcher/schema/logical_keywords_test.cpp b/src/mongo/db/matcher/schema/logical_keywords_test.cpp new file mode 100644 index 00000000000..1dbb709d4bf --- /dev/null +++ b/src/mongo/db/matcher/schema/logical_keywords_test.cpp @@ -0,0 +1,271 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/bson/json.h" +#include "mongo/db/matcher/schema/json_schema_parser.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +#define ASSERT_SERIALIZES_TO(match, expected) \ + do { \ + BSONObjBuilder bob; \ + match->serialize(&bob); \ + ASSERT_BSONOBJ_EQ(bob.obj(), expected); \ + } while (false) + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseIfAllOfIsNotAnArray) { + BSONObj schema = fromjson("{properties: {foo: {allOf: 'foo'}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseAllOfIfArrayContainsInvalidSchema) { + BSONObj schema = fromjson("{properties: {foo: {allOf: [{type: {}}]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseAllOfIfArrayIsEmpty) { + BSONObj schema = fromjson("{properties: {foo: {allOf: []}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::BadValue); +} + +TEST(JSONSchemaLogicalKeywordTest, AllOfTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {foo: {allOf: [{minimum: 0}, {maximum: 10}]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + auto expectedResult = fromjson( + R"({ + $or: + [{foo: {$not: {$exists: true}}}, + { + $and : [ + {$or: [ {foo: {$not: {$_internalSchemaType: ["number"]}}}, {foo: {$gte: 0}}]}, + {$or: [ {foo: {$not: {$_internalSchemaType: ["number"]}}}, {foo: {$lte: 10}}]} + ] + }] + })"); + ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); +} + +TEST(JSONSchemaLogicalKeywordTest, TopLevelAllOfTranslatesCorrectly) { + BSONObj schema = fromjson("{allOf: [{properties: {foo: {type: 'string'}}}]}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{foo: {$not: {$exists: true}}}, {foo: {$_internalSchemaType: [2]}}] + })")); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseIfAnyOfIsNotAnArray) { + BSONObj schema = fromjson("{properties: {foo: {anyOf: 'foo'}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseAnyOfIfArrayContainsInvalidSchema) { + BSONObj schema = fromjson("{properties: {foo: {anyOf: [{type: {}}]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseAnyOfIfArrayIsEmpty) { + BSONObj schema = fromjson("{properties: {foo: {anyOf: []}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::BadValue); +} + +TEST(JSONSchemaLogicalKeywordTest, AnyOfTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {foo: {anyOf: [{type: 'number'}, {type: 'string'}]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{foo: {$not: {$exists: true}}}, + {foo: {$_internalSchemaType: ["number"]}}, + {foo: {$_internalSchemaType: [2]}}] + })")); +} + +TEST(JSONSchemaLogicalKeywordTest, TopLevelAnyOfTranslatesCorrectly) { + BSONObj schema = fromjson("{anyOf: [{properties: {foo: {type: 'string'}}}]}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{foo: {$not: {$exists: true}}}, {foo: {$_internalSchemaType: [2]}}] + })")); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseIfOneOfIsNotAnArray) { + BSONObj schema = fromjson("{properties: {foo: {oneOf: 'foo'}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseOneOfIfArrayContainsInvalidSchema) { + BSONObj schema = fromjson("{properties: {foo: {oneOf: [{type: {}}]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseOneOfIfArrayIsEmpty) { + BSONObj schema = fromjson("{properties: {foo: {oneOf: []}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::BadValue); +} + +TEST(JSONSchemaLogicalKeywordTest, OneOfTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {foo: {oneOf: [{minimum: 0}, {maximum: 10}]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{foo: {$not: {$exists: true}}}, + { + $_internalSchemaXor : [ + {$or: [ {foo: {$not: {$_internalSchemaType: ["number"]}}}, {foo: {$gte : 0}}]}, + {$or: [ {foo: {$not: {$_internalSchemaType: ["number"]}}}, {foo: {$lte : 10}}]} + ] + }] + })")); +} + +TEST(JSONSchemaLogicalKeywordTest, TopLevelOneOfTranslatesCorrectly) { + BSONObj schema = fromjson("{oneOf: [{properties: {foo: {type: 'string'}}}]}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{foo: {$not: {$exists: true}}}, {foo: {$_internalSchemaType: [2]}}] + })")); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseIfNotIsNotAnObject) { + BSONObj schema = fromjson("{properties: {foo: {not: 'foo'}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseNotIfObjectContainsInvalidSchema) { + BSONObj schema = fromjson("{properties: {foo: {not: {type: {}}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaLogicalKeywordTest, NotTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {foo: {not: {type: 'number'}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{foo: {$not: {$exists: true}}}, {foo: {$not: {$_internalSchemaType: ['number']}}}] + })")); +} + +TEST(JSONSchemaLogicalKeywordTest, TopLevelNotTranslatesCorrectly) { + BSONObj schema = fromjson("{not: {properties: {foo: {type: 'string'}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $nor: + [{$or: [ {foo: {$not: {$exists: true}}}, {foo: {$_internalSchemaType: [2]}}]}] + })")); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseIfEnumIsNotAnArray) { + BSONObj schema = fromjson("{properties: {foo: {enum: 'foo'}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseEnumIfArrayIsEmpty) { + BSONObj schema = fromjson("{properties: {foo: {enum: []}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaLogicalKeywordTest, FailsToParseEnumIfArrayContainsDuplicateValue) { + BSONObj schema = fromjson("{properties: {foo: {enum: [1, 2, 1]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); + + schema = fromjson("{properties: {foo: {enum: [{a: 1, b: 1}, {b: 1, a: 1}]}}}"); + result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaLogicalKeywordTest, EnumTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {foo: {enum: [1, '2', [3]]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{foo: {$not: {$exists: true}}}, + {foo: {$_internalSchemaEq: 1}}, + {foo: {$_internalSchemaEq: "2"}}, + {foo: {$_internalSchemaEq: [3]}}] + })")); +} + +TEST(JSONSchemaLogicalKeywordTest, TopLevelEnumTranslatesCorrectly) { + BSONObj schema = fromjson("{enum: [1, {foo: 1}]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{$_internalSchemaRootDocEq: {foo: 1}}")); +} + +TEST(JSONSchemaLogicalKeywordTest, TopLevelEnumWithZeroObjectsTranslatesCorrectly) { + BSONObj schema = fromjson("{enum: [1, 'impossible', true]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{$alwaysFalse: 1}")); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/object_keywords_test.cpp b/src/mongo/db/matcher/schema/object_keywords_test.cpp new file mode 100644 index 00000000000..3829f9f68ff --- /dev/null +++ b/src/mongo/db/matcher/schema/object_keywords_test.cpp @@ -0,0 +1,939 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/bson/json.h" +#include "mongo/db/matcher/expression_always_boolean.h" +#include "mongo/db/matcher/schema/json_schema_parser.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +#define ASSERT_SERIALIZES_TO(match, expected) \ + do { \ + BSONObjBuilder bob; \ + match->serialize(&bob); \ + ASSERT_BSONOBJ_EQ(bob.obj(), expected); \ + } while (false) + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfTypeIsNotAString) { + BSONObj schema = fromjson("{type: 1}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseNicelyIfTypeIsKnownUnsupportedAlias) { + BSONObj schema = fromjson("{type: 'integer'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema type 'integer' is not currently supported"); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseUnknownKeyword) { + BSONObj schema = fromjson("{unknown: 1}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfPropertiesIsNotAnObject) { + BSONObj schema = fromjson("{properties: 1}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfPropertiesIsNotAnObjectWithType) { + BSONObj schema = fromjson("{type: 'string', properties: 1}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfParticularPropertyIsNotAnObject) { + BSONObj schema = fromjson("{properties: {foo: 1}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfKeywordIsDuplicated) { + BSONObj schema = BSON("type" + << "object" + << "type" + << "object"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaObjectKeywordTest, EmptySchemaTranslatesCorrectly) { + BSONObj schema = fromjson("{}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); +} + +TEST(JSONSchemaObjectKeywordTest, TypeObjectTranslatesCorrectly) { + BSONObj schema = fromjson("{type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{}")); +} + +TEST(JSONSchemaObjectKeywordTest, NestedTypeObjectTranslatesCorrectly) { + BSONObj schema = + fromjson("{properties: {a: {type: 'object', properties: {b: {type: 'string'}}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO( + optimizedResult, + fromjson("{$or: [{a: {$not: {$exists: true }}}, {$and: [{a: {$_internalSchemaObjectMatch: " + "{$or: [{b: {$not: {$exists: true}}}, {b: {$_internalSchemaType: [2]}}]}}}, {a: " + "{$_internalSchemaType: [3]}}]}]}")); +} + +TEST(JSONSchemaObjectKeywordTest, TopLevelNonObjectTypeTranslatesCorrectly) { + BSONObj schema = fromjson("{type: 'string'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, BSON(AlwaysFalseMatchExpression::kName << 1)); +} + +TEST(JSONSchemaObjectKeywordTest, TypeNumberTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {num: {type: 'number'}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, + fromjson("{$or: [{num: {$not: {$exists: true }}}, {num: " + "{ $_internalSchemaType: [ 'number' ]}}]}")); +} + +TEST(JSONSchemaObjectKeywordTest, RequiredFailsToParseIfNotAnArray) { + BSONObj schema = fromjson("{required: 'field'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaObjectKeywordTest, RequiredFailsToParseArrayIsEmpty) { + BSONObj schema = fromjson("{required: []}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaObjectKeywordTest, RequiredFailsToParseIfArrayContainsNonString) { + BSONObj schema = fromjson("{required: ['foo', 1]}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaObjectKeywordTest, RequiredFailsToParseIfArrayContainsDuplicates) { + BSONObj schema = fromjson("{required: ['foo', 'bar', 'foo']}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaObjectKeywordTest, TopLevelRequiredTranslatesCorrectly) { + BSONObj schema = fromjson("{required: ['foo', 'bar']}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, + fromjson("{$and: [{bar: {$exists: true}}, {foo: {$exists: true}}]}")); +} + +TEST(JSONSchemaObjectKeywordTest, TopLevelRequiredTranslatesCorrectlyWithProperties) { + BSONObj schema = fromjson("{required: ['foo'], properties: {foo: {type: 'number'}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $and: [ + {foo: {$_internalSchemaType: ['number']}}, + {foo: {$exists: true}} + ] + })")); +} + +TEST(JSONSchemaObjectKeywordTest, RequiredTranslatesCorrectlyWithMultipleElements) { + BSONObj schema = fromjson("{properties: {x: {required: ['y', 'z']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{x: {$not: {$exists: true}}}, {x: {$not: {$_internalSchemaType: [3]}}}, { + x: { + $_internalSchemaObjectMatch: + {$and: [ {y: {$exists: true}}, {z: {$exists: true}}]} + } + }] + })")); +} + +TEST(JSONSchemaObjectKeywordTest, RequiredTranslatesCorrectlyInsideProperties) { + BSONObj schema = fromjson("{properties: {x: {required: ['y']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{x: {$not: {$exists: true}}}, + {x: {$not: {$_internalSchemaType: [3]}}}, + {x: {$_internalSchemaObjectMatch: {y: {$exists: true}}}}] + })")); +} + +TEST(JSONSchemaObjectKeywordTest, + RequiredTranslatesCorrectlyInsidePropertiesWithSiblingProperties) { + BSONObj schema = + fromjson("{properties: {x:{required: ['y'], properties: {y: {type: 'number'}}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + auto expectedResult = fromjson( + R"({ + $or: + [{x: {$not: {$exists: true}}}, + { + $and: [ + { + $or: [ + {x: {$not: {$_internalSchemaType: [3]}}}, + {x: {$_internalSchemaObjectMatch: {y : {$_internalSchemaType: ["number"]}}}} + ] + }, + { + $or: [ + {x: {$not: {$_internalSchemaType: [3]}}}, + {x: {$_internalSchemaObjectMatch: {y: {$exists: true}}}} + ] + } + ] + }] + })"); + ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); +} + +TEST(JSONSchemaObjectKeywordTest, SharedJsonAndBsonTypeAliasesTranslateIdentically) { + for (auto&& mapEntry : MatcherTypeSet::kJsonSchemaTypeAliasMap) { + auto typeAlias = mapEntry.first; + // JSON Schema spells its bool type as "boolean", whereas MongoDB calls it "bool". + auto bsonTypeAlias = + (typeAlias == JSONSchemaParser::kSchemaTypeBoolean) ? "bool" : typeAlias; + + BSONObj typeSchema = BSON("properties" << BSON("f" << BSON("type" << typeAlias))); + BSONObj bsonTypeSchema = + BSON("properties" << BSON("f" << BSON("bsonType" << bsonTypeAlias))); + auto typeResult = JSONSchemaParser::parse(new ExpressionContextForTest(), typeSchema); + ASSERT_OK(typeResult.getStatus()); + auto bsonTypeResult = + JSONSchemaParser::parse(new ExpressionContextForTest(), bsonTypeSchema); + ASSERT_OK(bsonTypeResult.getStatus()); + + BSONObjBuilder typeBuilder; + MatchExpression::optimize(std::move(typeResult.getValue()))->serialize(&typeBuilder); + + BSONObjBuilder bsonTypeBuilder; + MatchExpression::optimize(std::move(bsonTypeResult.getValue())) + ->serialize(&bsonTypeBuilder); + + ASSERT_BSONOBJ_EQ(typeBuilder.obj(), bsonTypeBuilder.obj()); + } +} + +TEST(JSONSchemaObjectKeywordTest, MinPropertiesFailsToParseIfNotNumber) { + BSONObj schema = fromjson("{minProperties: null}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, MaxPropertiesFailsToParseIfNotNumber) { + BSONObj schema = fromjson("{maxProperties: null}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, MinPropertiesFailsToParseIfNegative) { + BSONObj schema = fromjson("{minProperties: -2}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, MaxPropertiesFailsToParseIfNegative) { + BSONObj schema = fromjson("{maxProperties: -2}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, MinPropertiesFailsToParseIfNotAnInteger) { + BSONObj schema = fromjson("{minProperties: 1.1}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, MaxPropertiesFailsToParseIfNotAnInteger) { + BSONObj schema = fromjson("{maxProperties: 1.1}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, TopLevelMinPropertiesTranslatesCorrectly) { + BSONObj schema = fromjson("{minProperties: 0}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{$_internalSchemaMinProperties: 0}")); +} + +TEST(JSONSchemaObjectKeywordTest, TopLevelMaxPropertiesTranslatesCorrectly) { + BSONObj schema = fromjson("{maxProperties: 0}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson("{$_internalSchemaMaxProperties: 0}")); +} + +TEST(JSONSchemaObjectKeywordTest, NestedMinPropertiesTranslatesCorrectly) { + BSONObj schema = + fromjson("{properties: {obj: {type: 'object', minProperties: 2}}, required: ['obj']}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + auto expectedResult = fromjson( + R"({ + $and: [ + {obj: {$exists: true}}, + {obj: {$_internalSchemaObjectMatch: {$_internalSchemaMinProperties: 2}}}, + {obj: {$_internalSchemaType: [3]}} + ] + })"); + ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); +} + +TEST(JSONSchemaObjectKeywordTest, NestedMaxPropertiesTranslatesCorrectly) { + BSONObj schema = + fromjson("{properties: {obj: {type: 'object', maxProperties: 2}}, required: ['obj']}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + auto expectedResult = fromjson( + R"({ + $and: [ + {obj: {$exists: true}}, + {obj: {$_internalSchemaObjectMatch: {$_internalSchemaMaxProperties: 2}}}, + {obj: {$_internalSchemaType: [3]}} + ] + })"); + ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); +} + +TEST(JSONSchemaObjectKeywordTest, NestedMinPropertiesTranslatesCorrectlyWithoutRequired) { + BSONObj schema = fromjson("{properties: {obj: {type: 'object', minProperties: 2}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + auto expectedResult = fromjson(R"( + { + $or: + [{obj: {$not: {$exists: true}}}, { + $and: [ + {obj: {$_internalSchemaObjectMatch: {$_internalSchemaMinProperties: 2}}}, + {obj: {$_internalSchemaType: [3]}} + ] + }] + })"); + ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); +} + +TEST(JSONSchemaObjectKeywordTest, NestedMaxPropertiesTranslatesCorrectlyWithoutRequired) { + BSONObj schema = fromjson("{properties: {obj: {type: 'object', maxProperties: 2}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + auto expectedResult = fromjson(R"( + { + $or: + [{obj: {$not: {$exists: true}}}, { + $and: [ + {obj: {$_internalSchemaObjectMatch: {$_internalSchemaMaxProperties: 2}}}, + {obj: {$_internalSchemaType: [3]}} + ] + }] + })"); + ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfTypeArrayHasRepeatedAlias) { + BSONObj schema = fromjson("{properties: {obj: {type: ['object', 'string', 'object']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfBsonTypeArrayHasRepeatedAlias) { + BSONObj schema = fromjson("{properties: {obj: {bsonType: ['object', 'string', 'object']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfTypeArrayIsEmpty) { + BSONObj schema = fromjson("{properties: {obj: {type: []}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfBsonTypeArrayIsEmpty) { + BSONObj schema = fromjson("{properties: {obj: {bsonType: []}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfTypeArrayContainsNonString) { + BSONObj schema = fromjson("{properties: {obj: {type: [1]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfBsonTypeArrayContainsNonString) { + BSONObj schema = fromjson("{properties: {obj: {bsonType: [1]}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfTypeArrayContainsUnknownAlias) { + BSONObj schema = fromjson("{properties: {obj: {type: ['objectId']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseNicelyIfTypeArrayContainsKnownUnsupportedAlias) { + BSONObj schema = fromjson("{properties: {obj: {type: ['number', 'integer']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema type 'integer' is not currently supported"); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfBsonTypeArrayContainsUnknownAlias) { + BSONObj schema = fromjson("{properties: {obj: {bsonType: ['unknown']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, CanTranslateTopLevelTypeArrayWithoutObject) { + BSONObj schema = fromjson("{type: ['number', 'string']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_SERIALIZES_TO(result.getValue(), BSON(AlwaysFalseMatchExpression::kName << 1)); +} + +TEST(JSONSchemaObjectKeywordTest, CanTranslateTopLevelBsonTypeArrayWithoutObject) { + BSONObj schema = fromjson("{bsonType: ['number', 'string']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_SERIALIZES_TO(result.getValue(), BSON(AlwaysFalseMatchExpression::kName << 1)); +} + +TEST(JSONSchemaObjectKeywordTest, CanTranslateTopLevelTypeArrayWithObject) { + BSONObj schema = fromjson("{type: ['number', 'object']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_SERIALIZES_TO(result.getValue(), fromjson("{}")); +} + +TEST(JSONSchemaObjectKeywordTest, CanTranslateTopLevelBsonTypeArrayWithObject) { + BSONObj schema = fromjson("{bsonType: ['number', 'object']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_SERIALIZES_TO(result.getValue(), fromjson("{}")); +} + +TEST(JSONSchemaObjectKeywordTest, CanTranslateNestedTypeArray) { + BSONObj schema = fromjson("{properties: {a: {type: ['number', 'object']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{a: {$not: {$exists: true}}}, {a: {$_internalSchemaType: [ "number", 3 ]}}] + })")); +} + +TEST(JSONSchemaObjectKeywordTest, CanTranslateNestedBsonTypeArray) { + BSONObj schema = fromjson("{properties: {a: {bsonType: ['number', 'objectId']}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{a: {$not: {$exists: true}}}, {a: {$_internalSchemaType: [ "number", 7 ]}}] + })")); +} + +TEST(JSONSchemaObjectKeywordTest, DependenciesFailsToParseIfNotAnObject) { + BSONObj schema = fromjson("{dependencies: []}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, DependenciesFailsToParseIfDependencyIsNotObjectOrArray) { + BSONObj schema = fromjson("{dependencies: {a: ['b'], bad: 1}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, DependenciesFailsToParseIfNestedSchemaIsInvalid) { + BSONObj schema = fromjson("{dependencies: {a: {invalid: 1}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, PropertyDependencyFailsToParseIfEmptyArray) { + BSONObj schema = fromjson("{dependencies: {a: []}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, PropertyDependencyFailsToParseIfArrayContainsNonStringElement) { + BSONObj schema = fromjson("{dependencies: {a: ['b', 1]}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, PropertyDependencyFailsToParseIfRepeatedArrayElement) { + BSONObj schema = fromjson("{dependencies: {a: ['b', 'b']}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, TopLevelSchemaDependencyTranslatesCorrectly) { + BSONObj schema = fromjson("{dependencies: {a: {properties: {b: {type: 'string'}}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $_internalSchemaCond: + [{a: {$exists: true}}, + {$or: [ {b: {$not: {$exists: true}}}, {b: {$_internalSchemaType: [2]}}]}, + {$alwaysTrue: 1}] + })")); +} + +TEST(JSONSchemaObjectKeywordTest, TopLevelPropertyDependencyTranslatesCorrectly) { + BSONObj schema = fromjson("{dependencies: {a: ['b', 'c']}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $_internalSchemaCond: [ + {a: {$exists: true}}, + { + $and: [ + {b: {$exists: true}}, + {c: {$exists: true}} + ] + }, + {$alwaysTrue: 1} + ] + })")); +} + +TEST(JSONSchemaObjectKeywordTest, NestedSchemaDependencyTranslatesCorrectly) { + BSONObj schema = + fromjson("{properties: {a: {dependencies: {b: {properties: {c: {type: 'object'}}}}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{a: {$not: {$exists: true}}}, { + $_internalSchemaCond: [ + {a: {$_internalSchemaObjectMatch : {b: {$exists: true}}}}, + { + $or : [ + {a: {$not: {$_internalSchemaType: [3]}}}, + { + a: { + $_internalSchemaObjectMatch : { + $or : [ + {c: {$not: {$exists: true}}}, + {c: {$_internalSchemaType: [3]}} + ] + } + } + } + ] + }, + {$alwaysTrue : 1} + ] + }] + })")); +} + +TEST(JSONSchemaObjectKeywordTest, NestedPropertyDependencyTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {a: {dependencies: {b: ['c', 'd']}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + auto expectedResult = fromjson(R"( + { + $or: + [{a: {$not: {$exists: true}}}, { + $_internalSchemaCond: [ + {a: {$_internalSchemaObjectMatch : {b : {$exists : true}}}}, + { + $and: [ + {a: {$_internalSchemaObjectMatch: {c: {$exists: true}}}}, + {a: {$_internalSchemaObjectMatch: {d: {$exists: true}}}} + ] + }, + {$alwaysTrue: 1} + ] + }] + })"); + ASSERT_SERIALIZES_TO(optimizedResult, expectedResult); +} + +TEST(JSONSchemaObjectKeywordTest, EmptyDependenciesTranslatesCorrectly) { + BSONObj schema = fromjson("{dependencies: {}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_SERIALIZES_TO(result.getValue(), fromjson("{$and: [{}]}")); +} + +TEST(JSONSchemaObjectKeywordTest, UnsupportedKeywordsFailNicely) { + auto result = + JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{default: {}}")); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword 'default' is not currently supported"); + + result = JSONSchemaParser::parse(new ExpressionContextForTest(), + fromjson("{definitions: {numberField: {type: 'number'}}}")); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword 'definitions' is not currently supported"); + + result = JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{format: 'email'}")); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword 'format' is not currently supported"); + + result = JSONSchemaParser::parse(new ExpressionContextForTest(), + fromjson("{id: 'someschema.json'}")); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword 'id' is not currently supported"); + + result = JSONSchemaParser::parse(new ExpressionContextForTest(), + BSON("$ref" + << "#/definitions/positiveInt")); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword '$ref' is not currently supported"); + + result = JSONSchemaParser::parse(new ExpressionContextForTest(), + fromjson("{$schema: 'hyper-schema'}")); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword '$schema' is not currently supported"); + + result = + JSONSchemaParser::parse(new ExpressionContextForTest(), + fromjson("{$schema: 'http://json-schema.org/draft-04/schema#'}")); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword '$schema' is not currently supported"); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfDescriptionIsNotAString) { + auto result = + JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{description: {}}")); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, CorrectlyParsesDescriptionAsString) { + auto result = + JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{description: 'str'}")); + ASSERT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, CorrectlyParsesNestedDescriptionAsString) { + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), + fromjson("{properties: {a: {description: 'str'}}}")); + ASSERT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseIfTitleIsNotAString) { + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{title: {}}")); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, CorrectlyParsesTitleAsString) { + auto result = + JSONSchemaParser::parse(new ExpressionContextForTest(), fromjson("{title: 'str'}")); + ASSERT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, CorrectlyParsesNestedTitleAsString) { + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), + fromjson("{properties: {a: {title: 'str'}}}")); + ASSERT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, PatternPropertiesFailsToParseIfNotObject) { + BSONObj schema = fromjson("{patternProperties: 1}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, PatternPropertiesFailsToParseIfOnePropertyIsNotObject) { + BSONObj schema = fromjson("{patternProperties: {a: {}, b: 1}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, PatternPropertiesFailsToParseIfNestedSchemaIsInvalid) { + BSONObj schema = fromjson("{patternProperties: {a: {invalid: 1}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, PatternPropertiesFailsToParseIfPropertyNameIsAnInvalidRegex) { + BSONObj schema = fromjson("{patternProperties: {'[': {}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, AdditionalPropertiesFailsToParseIfNotBoolOrString) { + BSONObj schema = fromjson("{additionalProperties: 1}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, AdditionalPropertiesFailsToParseIfNestedSchemaIsInvalid) { + BSONObj schema = fromjson("{additionalProperties: {invalid: 1}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_NOT_OK(result.getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, TopLevelPatternPropertiesTranslatesCorrectly) { + BSONObj schema = + fromjson("{patternProperties: {'^a': {type: 'number'}, '^b': {type: 'string'}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $_internalSchemaAllowedProperties: { + properties: [], + namePlaceholder: "i", + patternProperties: [ + { + regex: /^a/, + expression: { + i: {$_internalSchemaType: ['number']} + } + }, + { + regex: /^b/, + expression: { + i: {$_internalSchemaType: [2]} + } + } + ], + otherwise: {$alwaysTrue: 1} + } + })")); +} + +TEST(JSONSchemaObjectKeywordTest, TopLevelAdditionalPropertiesFalseTranslatesCorrectly) { + BSONObj schema = fromjson("{additionalProperties: false}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $_internalSchemaAllowedProperties: { + properties: [], + namePlaceholder: "i", + patternProperties: [], + otherwise: {$alwaysFalse: 1} + } + })")); +} + +TEST(JSONSchemaObjectKeywordTest, TopLevelAdditionalPropertiesTrueTranslatesCorrectly) { + BSONObj schema = fromjson("{additionalProperties: true}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $_internalSchemaAllowedProperties: { + properties: [], + namePlaceholder: "i", + patternProperties: [], + otherwise: {$alwaysTrue: 1} + } + })")); +} + +TEST(JSONSchemaObjectKeywordTest, TopLevelAdditionalPropertiesTypeNumberTranslatesCorrectly) { + BSONObj schema = fromjson("{additionalProperties: {type: 'number'}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $_internalSchemaAllowedProperties: { + properties: [], + namePlaceholder: "i", + patternProperties: [], + otherwise: {i: {$_internalSchemaType: ['number']}} + } + })")); +} + +TEST(JSONSchemaObjectKeywordTest, NestedAdditionalPropertiesTranslatesCorrectly) { + BSONObj schema = fromjson("{properties: {obj: {additionalProperties: {type: 'number'}}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{obj: {$not: {$exists: true}}}, {obj: {$not: {$_internalSchemaType: [3]}}}, { + obj: { + $_internalSchemaObjectMatch: { + $_internalSchemaAllowedProperties: { + properties: [], + namePlaceholder: "i", + patternProperties: [], + otherwise: {i: {$_internalSchemaType: ["number"]}} + } + } + } + }] + })")); +} + +TEST(JSONSchemaObjectKeywordTest, + PropertiesPatternPropertiesAndAdditionalPropertiesTranslateCorrectlyTogether) { + BSONObj schema = fromjson( + "{properties: {a: {}, b: {}}, patternProperties: {'^c': {}}, additionalProperties: false}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $_internalSchemaAllowedProperties: { + properties: ["a", "b"], + namePlaceholder: "i", + patternProperties: [ + {regex: /^c/, expression: {}} + ], + otherwise: {$alwaysFalse: 1} + } + })")); +} + +TEST(JSONSchemaObjectKeywordTest, + PropertiesPatternPropertiesAdditionalPropertiesAndRequiredTranslateCorrectlyTogether) { + BSONObj schema = fromjson( + "{properties: {a: {}, b: {}}, required: ['a'], patternProperties: {'^c': {}}, " + "additionalProperties: false}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $and: [ + { + $_internalSchemaAllowedProperties: { + properties: ["a", "b"], + namePlaceholder: "i", + patternProperties: [ + {regex: /^c/, expression: {}} + ], + otherwise: {$alwaysFalse: 1} + } + }, + {a: {$exists: true}} + ] + })")); +} + +TEST(JSONSchemaObjectKeywordTest, CorrectlyIgnoresUnknownKeywordsParameterIsSet) { + const auto ignoreUnknownKeywords = true; + + auto schema = fromjson("{ignored_keyword: 1}"); + ASSERT_OK(JSONSchemaParser::parse(new ExpressionContextForTest(), schema, ignoreUnknownKeywords) + .getStatus()); + + schema = fromjson("{properties: {a: {ignored_keyword: 1}}}"); + ASSERT_OK(JSONSchemaParser::parse(new ExpressionContextForTest(), schema, ignoreUnknownKeywords) + .getStatus()); + + schema = fromjson("{properties: {a: {oneOf: [{ignored_keyword: {}}]}}}"); + ASSERT_OK(JSONSchemaParser::parse(new ExpressionContextForTest(), schema, ignoreUnknownKeywords) + .getStatus()); +} + +TEST(JSONSchemaObjectKeywordTest, FailsToParseUnsupportedKeywordsWhenIgnoreUnknownParameterIsSet) { + const auto ignoreUnknownKeywords = true; + + auto result = JSONSchemaParser::parse( + new ExpressionContextForTest(), fromjson("{default: {}}"), ignoreUnknownKeywords); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword 'default' is not currently supported"); + + result = JSONSchemaParser::parse(new ExpressionContextForTest(), + fromjson("{definitions: {numberField: {type: 'number'}}}"), + ignoreUnknownKeywords); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword 'definitions' is not currently supported"); + + result = JSONSchemaParser::parse( + new ExpressionContextForTest(), fromjson("{format: 'email'}"), ignoreUnknownKeywords); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword 'format' is not currently supported"); + + result = JSONSchemaParser::parse( + new ExpressionContextForTest(), fromjson("{id: 'someschema.json'}"), ignoreUnknownKeywords); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword 'id' is not currently supported"); + + result = JSONSchemaParser::parse(new ExpressionContextForTest(), + BSON("$ref" + << "#/definitions/positiveInt"), + ignoreUnknownKeywords); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword '$ref' is not currently supported"); + + result = JSONSchemaParser::parse(new ExpressionContextForTest(), + fromjson("{$schema: 'hyper-schema'}"), + ignoreUnknownKeywords); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword '$schema' is not currently supported"); + + result = + JSONSchemaParser::parse(new ExpressionContextForTest(), + fromjson("{$schema: 'http://json-schema.org/draft-04/schema#'}"), + ignoreUnknownKeywords); + ASSERT_STRING_CONTAINS(result.getStatus().reason(), + "$jsonSchema keyword '$schema' is not currently supported"); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/matcher/schema/scalar_keywords_test.cpp b/src/mongo/db/matcher/schema/scalar_keywords_test.cpp new file mode 100644 index 00000000000..6de666dd3b9 --- /dev/null +++ b/src/mongo/db/matcher/schema/scalar_keywords_test.cpp @@ -0,0 +1,383 @@ +/** + * Copyright (C) 2019-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/bson/json.h" +#include "mongo/db/matcher/expression_always_boolean.h" +#include "mongo/db/matcher/schema/json_schema_parser.h" +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +#define ASSERT_SERIALIZES_TO(match, expected) \ + do { \ + BSONObjBuilder bob; \ + match->serialize(&bob); \ + ASSERT_BSONOBJ_EQ(bob.obj(), expected); \ + } while (false) + +TEST(JSONSchemaParserScalarTest, MaximumTranslatesCorrectlyWithTypeNumber) { + BSONObj schema = fromjson("{properties: {num: {type: 'number', maximum: 0}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, + fromjson("{$or: [{num: {$not: {$exists: true}}}, {$and: [{num: {$lte: " + "0}}, {num: {$_internalSchemaType: ['number']}}]}]}")); +} + +TEST(JSONSchemaParserScalarTest, MaximumTranslatesCorrectlyWithBsonTypeLong) { + BSONObj schema = + fromjson("{properties: {num: {bsonType: 'long', maximum: 0}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, + fromjson("{$or: [{num: {$not: {$exists: true}}}, {$and: [{num: {$lte: " + "0}}, {num: {$_internalSchemaType: [18]}}]}]}")); +} + +TEST(JSONSchemaParserScalarTest, MaximumTranslatesCorrectlyWithTypeString) { + BSONObj schema = fromjson("{properties: {num: {type: 'string', maximum: 0}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO( + optimizedResult, + fromjson("{$or: [{num: {$not: {$exists: true }}}, {num: {$_internalSchemaType: [2]}}]}")); +} + +TEST(JSONSchemaParserScalarTest, MaximumTranslatesCorrectlyWithNoType) { + BSONObj schema = fromjson("{properties: {num: {maximum: 0}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{num: {$not: {$exists : true}}}, + {num: {$not: {$_internalSchemaType: ["number"]}}}, + {num: {$lte: 0}}] + })")); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfMaximumIsNotANumber) { + BSONObj schema = fromjson("{maximum: 'foo'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfMaxLengthIsNotANumber) { + BSONObj schema = fromjson("{maxLength: 'foo'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfMaxLengthIsLessThanZero) { + BSONObj schema = fromjson("{maxLength: -1}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserScalarTest, MinimumTranslatesCorrectlyWithTypeNumber) { + BSONObj schema = fromjson("{properties: {num: {type: 'number', minimum: 0}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{num: {$not: {$exists : true}}}, + {$and: [ {num: {$gte: 0}}, {num: {$_internalSchemaType: ["number"]}}]}] + })")); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfMaxLengthIsNonIntegralDouble) { + BSONObj schema = + fromjson("{properties: {foo: {type: 'string', maxLength: 5.5}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserScalarTest, MaxLengthTranslatesCorrectlyWithIntegralDouble) { + BSONObj schema = + fromjson("{properties: {foo: {type: 'string', maxLength: 5.0}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{foo: {$not: {$exists: true}}}, + {$and: [ {foo: {$_internalSchemaMaxLength: 5}}, {foo: {$_internalSchemaType: [2]}}]}] + })")); +} + +TEST(JSONSchemaParserScalarTest, MaxLengthTranslatesCorrectlyWithTypeString) { + BSONObj schema = + fromjson("{properties: {foo: {type: 'string', maxLength: 5}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{foo: {$not: {$exists : true}}}, + {$and: [ {foo: {$_internalSchemaMaxLength: 5}}, {foo: {$_internalSchemaType: [2]}}]}] + })")); +} + +TEST(JSONSchemaParserScalarTest, MinimumTranslatesCorrectlyWithBsonTypeLong) { + BSONObj schema = + fromjson("{properties: {num: {bsonType: 'long', minimum: 0}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{num: {$not: {$exists: true}}}, + {$and: [ {num: {$gte: 0}}, {num: {$_internalSchemaType: [18]}}]}] + })")); +} + +TEST(JSONSchemaParserScalarTest, MinimumTranslatesCorrectlyWithTypeString) { + BSONObj schema = fromjson("{properties: {num: {type: 'string', minimum: 0}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{num: {$not: {$exists: true}}}, {num: {$_internalSchemaType: [2]}}] + })")); +} + + +TEST(JSONSchemaParserScalarTest, MinimumTranslatesCorrectlyWithNoType) { + BSONObj schema = fromjson("{properties: {num: {minimum: 0}}}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{num: {$not: {$exists: true}}}, + {num: {$not: {$_internalSchemaType: ["number"]}}}, + {num: {$gte: 0}}] + })")); +} + +TEST(JSONSchemaParserScalarTest, MaximumTranslatesCorrectlyWithExclusiveMaximumTrue) { + BSONObj schema = fromjson( + "{properties: {num: {bsonType: 'long', maximum: 0, exclusiveMaximum: true}}," + "type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{num: {$not: {$exists: true}}}, + {$and: [ {num: {$lt: 0}}, {num: {$_internalSchemaType: [18]}}]}] + })")); +} + +TEST(JSONSchemaParserScalarTest, MaximumTranslatesCorrectlyWithExclusiveMaximumFalse) { + BSONObj schema = fromjson( + "{properties: {num: {bsonType: 'long', maximum: 0, exclusiveMaximum: false}}," + "type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{num: {$not: {$exists: true}}}, + {$and: [ {num: {$lte: 0}}, {num: {$_internalSchemaType: [18]}}]}] + })")); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfExclusiveMaximumIsPresentButMaximumIsNot) { + BSONObj schema = fromjson("{exclusiveMaximum: true}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfExclusiveMaximumIsNotABoolean) { + BSONObj schema = fromjson("{maximum: 5, exclusiveMaximum: 'foo'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaParserScalarTest, MinimumTranslatesCorrectlyWithExclusiveMinimumTrue) { + BSONObj schema = fromjson( + "{properties: {num: {bsonType: 'long', minimum: 0, exclusiveMinimum: true}}," + "type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{num: {$not: {$exists: true}}}, + {$and: [ {num: {$gt: 0}}, {num: {$_internalSchemaType: [18]}}]}] + })")); +} + +TEST(JSONSchemaParserScalarTest, MinimumTranslatesCorrectlyWithExclusiveMinimumFalse) { + BSONObj schema = fromjson( + "{properties: {num: {bsonType: 'long', minimum: 0, exclusiveMinimum: false}}," + "type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{num: {$not: {$exists: true}}}, + {$and: [ {num: {$gte: 0}}, {num: {$_internalSchemaType: [18]}}]}] + })")); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfExclusiveMinimumIsPresentButMinimumIsNot) { + BSONObj schema = fromjson("{exclusiveMinimum: true}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfExclusiveMinimumIsNotABoolean) { + BSONObj schema = fromjson("{minimum: 5, exclusiveMinimum: 'foo'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfMinLengthIsNotANumber) { + BSONObj schema = fromjson("{minLength: 'foo'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfMinLengthIsLessThanZero) { + BSONObj schema = fromjson("{minLength: -1}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfMinLengthIsNonIntegralDouble) { + BSONObj schema = + fromjson("{properties: {foo: {type: 'string', minLength: 5.5}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserScalarTest, MinLengthTranslatesCorrectlyWithTypeString) { + BSONObj schema = + fromjson("{properties: {foo: {type: 'string', minLength: 5}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{foo: {$not: {$exists: true}}}, + {$and: [ {foo: {$_internalSchemaMinLength: 5}}, {foo: {$_internalSchemaType: [2]}}]}] + })")); +} + +TEST(JSONSchemaParserScalarTest, MinLengthTranslatesCorrectlyWithIntegralDouble) { + BSONObj schema = + fromjson("{properties: {foo: {type: 'string', minLength: 5.0}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{foo: {$not: {$exists: true}}}, + {$and: [ {foo: {$_internalSchemaMinLength: 5}}, {foo: {$_internalSchemaType: [2]}}]}] + })")); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfMinimumIsNotANumber) { + BSONObj schema = fromjson("{minimum: 'foo'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfPatternIsNotString) { + BSONObj schema = fromjson("{pattern: 6}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaParserScalarTest, PatternTranslatesCorrectlyWithString) { + BSONObj schema = + fromjson("{properties: {foo: {type: 'string', pattern: 'abc'}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + auto expected = + BSON("$or" << BSON_ARRAY( + BSON("foo" << BSON("$not" << BSON("$exists" << true))) + << BSON("$and" << BSON_ARRAY( + BSON("foo" << BSON("$regex" + << "abc")) + << BSON("foo" << BSON("$_internalSchemaType" << BSON_ARRAY(2))))))); + ASSERT_SERIALIZES_TO(optimizedResult, expected); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfMultipleOfIsNotANumber) { + BSONObj schema = fromjson("{multipleOf: 'foo'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::TypeMismatch); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfMultipleOfIsLessThanZero) { + BSONObj schema = fromjson("{multipleOf: -1}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserScalarTest, FailsToParseIfMultipleOfIsZero) { + BSONObj schema = fromjson("{multipleOf: 0}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_EQ(result.getStatus(), ErrorCodes::FailedToParse); +} + +TEST(JSONSchemaParserScalarTest, MultipleOfTranslatesCorrectlyWithTypeNumber) { + BSONObj schema = fromjson( + "{properties: {foo: {type: 'number', multipleOf: NumberDecimal('5.3')}}, type: 'object'}"); + auto result = JSONSchemaParser::parse(new ExpressionContextForTest(), schema); + ASSERT_OK(result.getStatus()); + auto optimizedResult = MatchExpression::optimize(std::move(result.getValue())); + ASSERT_SERIALIZES_TO(optimizedResult, fromjson(R"({ + $or: + [{foo: {$not: {$exists: true}}}, { + $and: [ + {foo: {$_internalSchemaFmod: [ NumberDecimal('5.3'), 0]}}, + {foo: {$_internalSchemaType: ["number"]}} + ] + }] + })")); +} + +} // namespace +} // namespace mongo |