340 lines
14 KiB
JavaScript
340 lines
14 KiB
JavaScript
// listCollections tests expect that a collection is not implicitly created after a drop.
|
|
// @tags: [assumes_no_implicit_collection_creation_after_drop, requires_non_retryable_commands]
|
|
|
|
/**
|
|
* Tests for JSON Schema document validation.
|
|
*/
|
|
(function() {
|
|
"use strict";
|
|
|
|
load("jstests/libs/assert_schema_match.js");
|
|
|
|
let coll = db.jstests_json_schema;
|
|
coll.drop();
|
|
|
|
assert.commandWorked(coll.insert({_id: 0, num: 3}));
|
|
assert.commandWorked(coll.insert({_id: 1, num: -3}));
|
|
assert.commandWorked(coll.insert({_id: 2, num: NumberInt(2)}));
|
|
assert.commandWorked(coll.insert({_id: 3, num: NumberInt(-2)}));
|
|
assert.commandWorked(coll.insert({_id: 4, num: NumberLong(1)}));
|
|
assert.commandWorked(coll.insert({_id: 5, num: NumberLong(-1)}));
|
|
assert.commandWorked(coll.insert({_id: 6, num: {}}));
|
|
assert.commandWorked(coll.insert({_id: 7, num: "str"}));
|
|
assert.commandWorked(coll.insert({_id: 8, num: "string"}));
|
|
assert.commandWorked(coll.insert({_id: 9}));
|
|
|
|
// Test that $jsonSchema fails to parse if its argument is not an object.
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: "foo"}).itcount();
|
|
});
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: []}).itcount();
|
|
});
|
|
|
|
// Test that $jsonSchema fails to parse if the value for the "type" keyword is not a string.
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {type: 3}}).itcount();
|
|
});
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {type: {}}}).itcount();
|
|
});
|
|
|
|
// Test that $jsonSchema fails to parse if the value for the "type" keyword is an unsupported
|
|
// alias.
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {type: 'integer'}}).itcount();
|
|
});
|
|
|
|
// Test that $jsonSchema fails to parse if the value for the properties keyword is not an
|
|
// object.
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {properties: 3}}).itcount();
|
|
});
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {properties: []}}).itcount();
|
|
});
|
|
|
|
// Test that $jsonSchema fails to parse if one of the properties named inside the argument for
|
|
// the properties keyword is not an object.
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {properties: {num: "number"}}}).itcount();
|
|
});
|
|
|
|
// Test that $jsonSchema fails to parse if the values for the maximum, maxLength, and
|
|
// minlength keywords are not numbers.
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {properties: {num: {maximum: "0"}}}}).itcount();
|
|
});
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {properties: {num: {maximum: {}}}}}).itcount();
|
|
});
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {properties: {num: {maxLength: "0"}}}}).itcount();
|
|
});
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {properties: {num: {maxLength: {}}}}}).itcount();
|
|
});
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {properties: {num: {minLength: "0"}}}}).itcount();
|
|
});
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {properties: {num: {minLength: {}}}}}).itcount();
|
|
});
|
|
|
|
// Test that the empty schema matches everything.
|
|
assert.eq(10, coll.find({$jsonSchema: {}}).itcount());
|
|
|
|
// Test that a schema just checking that the type of stored documents is "object" is legal and
|
|
// matches everything.
|
|
assert.eq(10, coll.find({$jsonSchema: {type: "object"}}).itcount());
|
|
|
|
// Test that schemas whose top-level type is not object matches nothing.
|
|
assert.eq(0, coll.find({$jsonSchema: {type: "string"}}).itcount());
|
|
assert.eq(0, coll.find({$jsonSchema: {bsonType: "long"}}).itcount());
|
|
assert.eq(0, coll.find({$jsonSchema: {bsonType: "objectId"}}).itcount());
|
|
|
|
// Test that type:"number" only matches numbers, or documents where the field is missing.
|
|
assert.eq([{_id: 0}, {_id: 1}, {_id: 2}, {_id: 3}, {_id: 4}, {_id: 5}, {_id: 9}],
|
|
coll.find({$jsonSchema: {properties: {num: {type: "number"}}}}, {_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Test that maximum restriction is enforced correctly.
|
|
assert.eq([{_id: 1}, {_id: 3}, {_id: 5}, {_id: 9}],
|
|
coll.find({$jsonSchema: {properties: {num: {type: "number", maximum: -1}}}}, {_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Repeat the test, but include an explicit top-level type:"object".
|
|
assert.eq(
|
|
[{_id: 1}, {_id: 3}, {_id: 5}, {_id: 9}],
|
|
coll.find({$jsonSchema: {type: "object", properties: {num: {type: "number", maximum: -1}}}},
|
|
{_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Test that type:"long" only matches longs, or documents where the field is missing.
|
|
assert.eq([{_id: 4}, {_id: 5}, {_id: 9}],
|
|
coll.find({$jsonSchema: {properties: {num: {bsonType: "long"}}}}, {_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Test that maximum restriction is enforced correctly with type:"long".
|
|
assert.eq([{_id: 5}, {_id: 9}],
|
|
coll.find({$jsonSchema: {properties: {num: {bsonType: "long", maximum: 0}}}}, {_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Test that maximum restriction without a numeric type specified only applies to numbers.
|
|
assert.eq(
|
|
[{_id: 1}, {_id: 3}, {_id: 5}, {_id: 6}, {_id: 7}, {_id: 8}, {_id: 9}],
|
|
coll.find({$jsonSchema: {properties: {num: {maximum: 0}}}}, {_id: 1}).sort({_id: 1}).toArray());
|
|
|
|
// Test that maximum restriction does nothing if a non-numeric type is also specified.
|
|
assert.eq([{_id: 7}, {_id: 8}, {_id: 9}],
|
|
coll.find({$jsonSchema: {properties: {num: {type: "string", maximum: 0}}}}, {_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Test that maxLength restriction doesn't return strings with length greater than maxLength.
|
|
assert.eq([{_id: 9}],
|
|
coll.find({$jsonSchema: {properties: {num: {type: "string", maxLength: 2}}}}, {_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Test that maxLength restriction returns strings with length less than or equal to maxLength.
|
|
assert.eq([{_id: 7}, {_id: 9}],
|
|
coll.find({$jsonSchema: {properties: {num: {type: "string", maxLength: 3}}}}, {_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Test that minLength restriction doesn't return strings with length less than minLength.
|
|
assert.eq([{_id: 8}, {_id: 9}],
|
|
coll.find({$jsonSchema: {properties: {num: {type: "string", minLength: 4}}}}, {_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Test that minLength restriction returns strings with length greater than or equal to
|
|
// minLength.
|
|
assert.eq([{_id: 7}, {_id: 8}, {_id: 9}],
|
|
coll.find({$jsonSchema: {properties: {num: {type: "string", minLength: 3}}}}, {_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Test that $jsonSchema fails to parse if the values for the pattern keyword is not a string.
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {properties: {num: {pattern: 0}}}}).itcount();
|
|
});
|
|
assert.throws(function() {
|
|
coll.find({$jsonSchema: {properties: {num: {pattern: {}}}}}).itcount();
|
|
});
|
|
|
|
// Tests that the pattern keyword only returns strings that match the regex pattern.
|
|
assert.eq([{_id: 8}, {_id: 9}],
|
|
coll.find({$jsonSchema: {properties: {num: {type: "string", pattern: "ing"}}}}, {_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert({_id: 0, obj: 3}));
|
|
assert.commandWorked(coll.insert({_id: 1, obj: {f1: {f3: "str"}, f2: "str"}}));
|
|
assert.commandWorked(coll.insert({_id: 2, obj: {f1: "str", f2: "str"}}));
|
|
assert.commandWorked(coll.insert({_id: 3, obj: {f1: 1, f2: "str"}}));
|
|
|
|
// Test that properties keyword can be used recursively, and that it does not apply when the
|
|
// field does not contain on object.
|
|
assert.eq([{_id: 0}, {_id: 1}],
|
|
coll.find({
|
|
$jsonSchema: {
|
|
properties: {
|
|
obj: {
|
|
properties: {
|
|
f1: {type: "object", properties: {f3: {type: "string"}}},
|
|
f2: {type: "string"}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Test that $jsonSchema can be combined with other operators in the match language.
|
|
assert.eq([{_id: 0}, {_id: 1}, {_id: 2}],
|
|
coll.find({
|
|
$or: [
|
|
{"obj.f1": "str"},
|
|
{
|
|
$jsonSchema: {
|
|
properties: {
|
|
obj: {
|
|
properties: {
|
|
f1: {type: "object", properties: {f3: {type: "string"}}},
|
|
f2: {type: "string"}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert({_id: 0, arr: 3}));
|
|
assert.commandWorked(coll.insert({_id: 1, arr: [1, "foo"]}));
|
|
assert.commandWorked(coll.insert({_id: 2, arr: [{a: 1}, {b: 2}]}));
|
|
assert.commandWorked(coll.insert({_id: 3, arr: []}));
|
|
assert.commandWorked(coll.insert({_id: 4, arr: {a: []}}));
|
|
|
|
// Test that the type:"array" restriction works as expected.
|
|
assert.eq([{_id: 1}, {_id: 2}, {_id: 3}],
|
|
coll.find({$jsonSchema: {properties: {arr: {type: "array"}}}}, {_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Test that type:"number" works correctly in the presence of arrays.
|
|
assert.eq([{_id: 0}],
|
|
coll.find({$jsonSchema: {properties: {arr: {type: "number"}}}}, {_id: 1})
|
|
.sort({_id: 1})
|
|
.toArray());
|
|
|
|
// Test that the following keywords fail to parse although present in the spec:
|
|
// - default
|
|
// - definitions
|
|
// - format
|
|
// - id
|
|
// - $ref
|
|
// - $schema
|
|
let res = coll.runCommand({find: coll.getName(), query: {$jsonSchema: {default: {_id: 0}}}});
|
|
assert.commandFailedWithCode(res, ErrorCodes.FailedToParse);
|
|
|
|
res = coll.runCommand(
|
|
{find: coll.getName(), query: {$jsonSchema: {definitions: {numberField: {type: "number"}}}}});
|
|
assert.commandFailedWithCode(res, ErrorCodes.FailedToParse);
|
|
|
|
res = coll.runCommand({find: coll.getName(), query: {$jsonSchema: {format: "email"}}});
|
|
assert.commandFailedWithCode(res, ErrorCodes.FailedToParse);
|
|
|
|
res = coll.runCommand({find: coll.getName(), query: {$jsonSchema: {id: "someschema.json"}}});
|
|
assert.commandFailedWithCode(res, ErrorCodes.FailedToParse);
|
|
|
|
res = coll.runCommand({
|
|
find: coll.getName(),
|
|
query: {$jsonSchema: {properties: {a: {$ref: "#/definitions/positiveInt"}}}}
|
|
});
|
|
assert.commandFailedWithCode(res, ErrorCodes.FailedToParse);
|
|
|
|
res = coll.runCommand({find: coll.getName(), query: {$jsonSchema: {$schema: "hyper-schema"}}});
|
|
assert.commandFailedWithCode(res, ErrorCodes.FailedToParse);
|
|
|
|
res = coll.runCommand({
|
|
find: coll.getName(),
|
|
query: {$jsonSchema: {$schema: "http://json-schema.org/draft-04/schema#"}}
|
|
});
|
|
assert.commandFailedWithCode(res, ErrorCodes.FailedToParse);
|
|
|
|
// Test that the following whitelisted keywords are verified as strings but otherwise ignored
|
|
// in a top-level schema:
|
|
// - description
|
|
// - title
|
|
assertSchemaMatch(coll, {description: "test"}, {}, true);
|
|
assertSchemaMatch(coll, {title: "insert title"}, {}, true);
|
|
|
|
// Repeat the test above with nested schema.
|
|
assertSchemaMatch(coll, {properties: {a: {description: "test"}}}, {a: {}}, true);
|
|
assertSchemaMatch(coll, {properties: {a: {title: "this is a's title"}}}, {a: {}}, true);
|
|
|
|
// Test that the $jsonSchema validator is correctly stored in the collection catalog.
|
|
coll.drop();
|
|
let schema = {properties: {a: {type: 'number'}, b: {minLength: 1}}};
|
|
assert.commandWorked(db.createCollection(coll.getName(), {validator: {$jsonSchema: schema}}));
|
|
|
|
let listCollectionsOutput = db.runCommand({listCollections: 1, filter: {name: coll.getName()}});
|
|
assert.commandWorked(listCollectionsOutput);
|
|
assert.eq(listCollectionsOutput.cursor.firstBatch[0].options.validator, {$jsonSchema: schema});
|
|
|
|
// Repeat the test above using the whitelisted metadata keywords.
|
|
coll.drop();
|
|
schema = {
|
|
title: "Test schema",
|
|
description: "Metadata keyword test"
|
|
};
|
|
assert.commandWorked(db.createCollection(coll.getName(), {validator: {$jsonSchema: schema}}));
|
|
|
|
listCollectionsOutput = db.runCommand({listCollections: 1, filter: {name: coll.getName()}});
|
|
assert.commandWorked(listCollectionsOutput);
|
|
assert.eq(listCollectionsOutput.cursor.firstBatch[0].options.validator, {$jsonSchema: schema});
|
|
|
|
// Repeat again with a nested schema.
|
|
coll.drop();
|
|
schema = {
|
|
properties: {a: {title: "Nested title", description: "Nested description"}}
|
|
};
|
|
assert.commandWorked(db.createCollection(coll.getName(), {validator: {$jsonSchema: schema}}));
|
|
|
|
listCollectionsOutput = db.runCommand({listCollections: 1, filter: {name: coll.getName()}});
|
|
assert.commandWorked(listCollectionsOutput);
|
|
assert.eq(listCollectionsOutput.cursor.firstBatch[0].options.validator, {$jsonSchema: schema});
|
|
|
|
// Test that $jsonSchema and various internal match expressions work correctly with sibling
|
|
// predicates.
|
|
coll.drop();
|
|
assert.commandWorked(coll.insert({_id: 1, a: 1, b: 1}));
|
|
assert.commandWorked(coll.insert({_id: 2, a: 2, b: 2}));
|
|
|
|
assert.eq(
|
|
1,
|
|
coll.find({$jsonSchema: {properties: {a: {type: "number"}}, required: ["a"]}, b: 1}).itcount());
|
|
assert.eq(1, coll.find({$or: [{$jsonSchema: {}, a: 1}, {b: 1}]}).itcount());
|
|
assert.eq(1, coll.find({$and: [{$jsonSchema: {}, a: 1}, {b: 1}]}).itcount());
|
|
|
|
assert.eq(1, coll.find({$_internalSchemaMinProperties: 3, b: 2}).itcount());
|
|
assert.eq(1, coll.find({$_internalSchemaMaxProperties: 3, b: 2}).itcount());
|
|
assert.eq(1, coll.find({$alwaysTrue: 1, b: 2}).itcount());
|
|
assert.eq(0, coll.find({$alwaysFalse: 1, b: 2}).itcount());
|
|
}());
|