mongo/jstests/aggregation/expressions/serialize_ejson.js

282 lines
9.8 KiB
JavaScript

/**
* Test the $serializeEJSON and $deserializeEJSON expressions.
* @tags: [
* requires_fcv_83
* ]
*/
import {describe, it} from "jstests/libs/mochalite.js";
import {assertDropCollection} from "jstests/libs/collection_drop_recreate.js";
const collName = jsTestName();
const coll = db[collName];
function mqlNestedJSON(depth, inner = null) {
depth = depth < 1 ? 1 : depth;
return {$toArray: "[".repeat(depth - 1) + JSON.stringify(inner) + "]".repeat(depth - 1)};
}
function getParameter(paramName) {
return assert.commandWorked(db.adminCommand({getParameter: 1, [paramName]: 1}))[paramName];
}
const maxBSONDepth = getParameter("maxBSONDepth");
function populateCollection(docs) {
assertDropCollection(db, collName);
const bulk = coll.initializeUnorderedBulkOp();
for (let i = 0; i < docs.length; i++) {
bulk.insert({_id: i, ...docs[i]});
}
assert.commandWorked(bulk.execute());
}
const commonSuccessTests = [
{bson: "string", canonical: "string", relaxed: "string"},
{
bson: NumberLong(1),
canonical: {$numberLong: "1"},
relaxed: NumberLong(1),
},
{
bson: NumberInt(1),
canonical: {$numberInt: "1"},
relaxed: NumberInt(1),
},
{
bson: {b: NumberInt(1)},
canonical: {b: {$numberInt: "1"}},
relaxed: {b: NumberInt(1)},
},
{
bson: {b: NumberLong(123)},
canonical: {b: {$numberLong: "123"}},
relaxed: {b: NumberLong(123)},
},
{
bson: [NumberInt(1), NumberInt(2)],
canonical: [{$numberInt: "1"}, {$numberInt: "2"}],
relaxed: [NumberInt(1), NumberInt(2)],
},
{
bson: 1,
canonical: {$numberDouble: "1"},
relaxed: 1,
},
{
bson: NaN,
canonical: {$numberDouble: "NaN"},
relaxed: {$numberDouble: "NaN"},
},
{
bson: ObjectId("507f1f77bcf86cd799439011"),
canonical: {$oid: "507f1f77bcf86cd799439011"},
relaxed: {$oid: "507f1f77bcf86cd799439011"},
},
{
bson: NumberDecimal("123"),
canonical: {$numberDecimal: "123"},
relaxed: {$numberDecimal: "123"},
},
{
bson: Code("function(){}"),
canonical: {$code: "function(){}"},
relaxed: {$code: "function(){}"},
},
{
bson: Code("function(){}", {a: NumberInt(1)}),
canonical: {$code: "function(){}", $scope: {a: {$numberInt: "1"}}},
relaxed: {$code: "function(){}", $scope: {a: {$numberInt: "1"}}},
},
{
bson: ISODate("2004-03-21T18:59:54.000Z"),
canonical: {$date: {$numberLong: "1079895594000"}},
relaxed: {$date: "2004-03-21T18:59:54.000Z"},
},
{
bson: null,
canonical: null,
relaxed: null,
},
{
bson: undefined,
canonical: {$undefined: true},
relaxed: {$undefined: true},
},
];
const serializeSuccessTests = [
...commonSuccessTests,
{
// bson is missing
canonical: null,
relaxed: null,
},
];
describe("$serializeEJSON", () => {
it("works with various inputs", () => {
populateCollection(serializeSuccessTests);
const results = coll
.aggregate([
{
$set: {
relaxedTest: {$serializeEJSON: {input: "$bson", relaxed: true}},
canonicalTest: {$serializeEJSON: {input: "$bson", relaxed: false}},
},
},
])
.toArray();
for (const result of results) {
assert(
bsonBinaryEqual(result.relaxedTest, result.relaxed),
`Failed relaxed test: ${tojson([result.relaxedTest, result.relaxed])}`,
);
assert(
bsonBinaryEqual(result.canonicalTest, result.canonical),
`Failed canonical test: ${tojson([result.canonicalTest, result.canonical])}`,
);
}
});
it("defaults relaxed to true", () => {
assert(
!bsonBinaryEqual(
coll.findOne({}, {a: {$serializeEJSON: {input: NumberInt(1)}}}).a,
coll.findOne({}, {a: {$serializeEJSON: {input: NumberInt(1), relaxed: false}}}).a,
),
);
});
it("rejects additional parameters", () => {
const err = assert.throwsWithCode(
() => coll.findOne({}, {a: {$serializeEJSON: {input: {}, foo: "bar"}}}),
ErrorCodes.FailedToParse,
);
assert.neq(-1, err.message.indexOf("$serializeEJSON found an unknown argument: foo"));
});
it("rejects non-boolean relaxed parameter", () => {
assert.doesNotThrow(() => coll.findOne({}, {a: {$serializeEJSON: {input: {}, relaxed: true}}}));
const err = assert.throwsWithCode(
() => coll.findOne({}, {a: {$serializeEJSON: {input: {}, relaxed: 1}}}),
ErrorCodes.BadValue,
);
assert.neq(-1, err.message.indexOf("Unexpected value for relaxed: 1"), err.message);
});
it("fails with conversion failure on depth limit", () => {
// Construct an expression with has an $serializeEJSON expression.
// The result is not returned, to avoid triggering the crashOnInvalidBSONError testing parameter.
function serializeTest(input, relaxed) {
return coll.findOne({}, {a: {$eq: [null, {$serializeEJSON: {input, relaxed}}]}});
}
assert.doesNotThrow(() => serializeTest(mqlNestedJSON(maxBSONDepth, 1), true));
assert.throwsWithCode(() => serializeTest(mqlNestedJSON(maxBSONDepth, 1), false), ErrorCodes.ConversionFailure);
});
it("uses onError on bad input", () => {
const badInput = mqlNestedJSON(maxBSONDepth, 1);
const res = coll.findOne({}, {a: {$serializeEJSON: {input: badInput, relaxed: false, onError: "depth limit"}}});
assert.eq(res.a, "depth limit");
});
it("supports roundtrip using $toString", () => {
const mqlEJSONRoundtrip = (input, relaxed) => ({
// BSON -> EJSON -> string -> EJSON -> BSON
$deserializeEJSON: {input: {$toObject: {$toString: {$serializeEJSON: {input, relaxed}}}}},
});
populateCollection(serializeSuccessTests);
const results = coll
.aggregate([
{
$set: {
relaxedTest: mqlEJSONRoundtrip({bson: "$bson"}, true),
canonicalTest: mqlEJSONRoundtrip({bson: "$bson"}, false),
},
},
])
.toArray();
for (const result of results) {
// Relaxed mode can lose information, so we check it conditionally.
if (bsonBinaryEqual(result.relaxed, result.canonical)) {
assert(
bsonBinaryEqual(result.relaxedTest.bson, result.bson),
`Failed relaxed test: ${tojson([result.relaxedTest.bson, result.bson])}`,
);
}
assert(
bsonBinaryEqual(result.canonicalTest.bson, result.bson),
`Failed canonical test: ${tojson([result.canonicalTest.bson, result.bson])}`,
);
}
});
});
const deserializeSuccessTests = [
...commonSuccessTests,
{
bson: null,
// canonical is missing
// relaxed is missing
},
];
const deserializeFailTests = [{$numberLong: 1}, {$numberLong: "bad"}, {$numberLong: "--1"}, {$numberLong: "Infinity"}];
describe("$deserializeEJSON", () => {
it("works with various inputs", () => {
populateCollection(deserializeSuccessTests);
const results = coll
.aggregate([
{
$set: {
relaxedTest: {$deserializeEJSON: {input: "$relaxed"}},
canonicalTest: {$deserializeEJSON: {input: "$canonical"}},
},
},
])
.toArray();
for (const result of results) {
assert(
bsonBinaryEqual(result.relaxedTest, result.bson),
`Failed relaxed test: ${tojson([result.relaxedTest, result.bson])}`,
);
assert(
bsonBinaryEqual(result.canonicalTest, result.bson),
`Failed canonical test: ${tojson([result.canonicalTest, result.bson])}`,
);
}
});
it("rejects additional parameters", () => {
const err = assert.throwsWithCode(
() => coll.findOne({}, {a: {$deserializeEJSON: {input: {}, foo: "bar"}}}),
ErrorCodes.FailedToParse,
);
assert.neq(-1, err.message.indexOf("$deserializeEJSON found an unknown argument: foo"));
});
it("supports deprecated symbol type", () => {
// The symbol type is not supported by this environment.
// Values of the type cannot be constructed and when returned, values are converted to strings.
// We can assert that it is constructed correctly in pipelines.
assertDropCollection(db, collName);
coll.insert({input: {$symbol: "value"}});
const res = coll.findOne(
{},
{value: {$deserializeEJSON: {input: "$input"}}, type: {$type: {$deserializeEJSON: {input: "$input"}}}},
);
assert.eq("symbol", res.type);
assert.eq("value", res.value);
});
it("fails with conversion failure", () => {
for (let test of deserializeFailTests) {
assert.throwsWithCode(
() => coll.findOne({}, {a: {$deserializeEJSON: {input: {$literal: test}}}}),
ErrorCodes.ConversionFailure,
);
}
});
it("uses onError on bad input", () => {
for (let test of deserializeFailTests) {
const res = assert.doesNotThrow(() =>
coll.findOne({}, {a: {$deserializeEJSON: {input: {$literal: test}, onError: "failed"}}}),
);
assert.eq(res.a, "failed");
}
});
});