From d9f0eaa243ceeba8dd82e9c3b07fd8bca10cfc44 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Fri, 13 Jan 2023 11:39:05 -0500 Subject: [PATCH] feat(NODE-4892)!: error on bson types not from this version (#543) Co-authored-by: Durran Jordan --- docs/upgrade-to-v5.md | 16 ++ src/binary.ts | 4 +- src/bson.ts | 3 +- src/bson_value.ts | 18 ++ src/code.ts | 4 +- src/constants.ts | 3 + src/db_ref.ts | 4 +- src/decimal128.ts | 4 +- src/double.ts | 4 +- src/error.ts | 15 ++ src/extended_json.ts | 23 ++- src/int_32.ts | 4 +- src/long.ts | 4 +- src/max_key.ts | 4 +- src/min_key.ts | 4 +- src/objectid.ts | 8 +- src/parser/calculate_size.ts | 29 ++- src/parser/serializer.ts | 107 +++++----- src/regexp.ts | 13 +- src/symbol.ts | 10 +- test/node/bson_older_versions_tests.js | 254 ------------------------ test/node/bson_test.js | 88 +------- test/node/bson_type_classes.test.ts | 100 ++++++++++ test/node/cross_compat.test.ts | 83 ++++++++ test/node/error.test.ts | 12 +- test/node/exports.test.ts | 2 + test/node/extended_json.test.ts | 228 ++------------------- test/node/parser/calculate_size.test.ts | 17 ++ test/node/parser/serializer.test.ts | 25 ++- test/node/symbol.test.ts | 44 ++++ test/node/type_identifier_tests.js | 9 +- test/types/bson.test-d.ts | 16 +- 32 files changed, 512 insertions(+), 647 deletions(-) create mode 100644 src/bson_value.ts delete mode 100644 test/node/bson_older_versions_tests.js create mode 100644 test/node/bson_type_classes.test.ts create mode 100644 test/node/cross_compat.test.ts create mode 100644 test/node/symbol.test.ts diff --git a/docs/upgrade-to-v5.md b/docs/upgrade-to-v5.md index 01be3b26..6e133ecc 100644 --- a/docs/upgrade-to-v5.md +++ b/docs/upgrade-to-v5.md @@ -288,3 +288,19 @@ try { throw error; } ``` + +### Explicit cross version incompatibility + +Starting with v5.0.0 of the BSON library instances of types from previous versions will throw an error when passed to the serializer. +This is to ensure that types are always serialized correctly and that there is no unexpected silent BSON serialization mistakes that could occur when mixing versions. +It's unexpected for any applications to have more than one version of the BSON library but with nested dependencies and re-exporting, this new error will illuminate those incorrect combinations. + +```ts +// npm install bson4@npm:bson@4 +// npm install bson5@npm:bson@5 +import { ObjectId } from 'bson4'; +import { serialize } from 'bson5'; + +serialize({ _id: new ObjectId() }); +// Uncaught BSONVersionError: Unsupported BSON version, bson types must be from bson 5.0 or later +``` diff --git a/src/binary.ts b/src/binary.ts index ea0dbce4..f95de920 100644 --- a/src/binary.ts +++ b/src/binary.ts @@ -4,6 +4,7 @@ import type { EJSONOptions } from './extended_json'; import { BSONError } from './error'; import { BSON_BINARY_SUBTYPE_UUID_NEW } from './constants'; import { ByteUtils } from './utils/byte_utils'; +import { BSONValue } from './bson_value'; /** @public */ export type BinarySequence = Uint8Array | number[]; @@ -27,7 +28,7 @@ export interface BinaryExtended { * @public * @category BSONType */ -export class Binary { +export class Binary extends BSONValue { get _bsontype(): 'Binary' { return 'Binary'; } @@ -75,6 +76,7 @@ export class Binary { * @param subType - the option binary type. */ constructor(buffer?: string | BinarySequence, subType?: number) { + super(); if ( !(buffer == null) && !(typeof buffer === 'string') && diff --git a/src/bson.ts b/src/bson.ts index b0684731..8ec644bf 100644 --- a/src/bson.ts +++ b/src/bson.ts @@ -49,7 +49,8 @@ export { BSONRegExp, Decimal128 }; -export { BSONError } from './error'; +export { BSONValue } from './bson_value'; +export { BSONError, BSONVersionError } from './error'; export { BSONType } from './constants'; export { EJSON } from './extended_json'; diff --git a/src/bson_value.ts b/src/bson_value.ts new file mode 100644 index 00000000..b36266e0 --- /dev/null +++ b/src/bson_value.ts @@ -0,0 +1,18 @@ +import { BSON_MAJOR_VERSION } from './constants'; + +/** @public */ +export abstract class BSONValue { + /** @public */ + public abstract get _bsontype(): string; + + /** @internal */ + get [Symbol.for('@@mdb.bson.version')](): typeof BSON_MAJOR_VERSION { + return BSON_MAJOR_VERSION; + } + + /** @public */ + public abstract inspect(): string; + + /** @internal */ + abstract toExtendedJSON(): unknown; +} diff --git a/src/code.ts b/src/code.ts index 5f2f51a7..48889f56 100644 --- a/src/code.ts +++ b/src/code.ts @@ -1,4 +1,5 @@ import type { Document } from './bson'; +import { BSONValue } from './bson_value'; /** @public */ export interface CodeExtended { @@ -11,7 +12,7 @@ export interface CodeExtended { * @public * @category BSONType */ -export class Code { +export class Code extends BSONValue { get _bsontype(): 'Code' { return 'Code'; } @@ -27,6 +28,7 @@ export class Code { * @param scope - an optional scope for the function. */ constructor(code: string | Function, scope?: Document | null) { + super(); this.code = code.toString(); this.scope = scope ?? null; } diff --git a/src/constants.ts b/src/constants.ts index 17fd8a55..50a3d42e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,6 @@ +/** @internal */ +export const BSON_MAJOR_VERSION = 5 as const; + /** @internal */ export const BSON_INT32_MAX = 0x7fffffff; /** @internal */ diff --git a/src/db_ref.ts b/src/db_ref.ts index a8997890..ebdcb61b 100644 --- a/src/db_ref.ts +++ b/src/db_ref.ts @@ -1,4 +1,5 @@ import type { Document } from './bson'; +import { BSONValue } from './bson_value'; import type { EJSONOptions } from './extended_json'; import type { ObjectId } from './objectid'; @@ -28,7 +29,7 @@ export function isDBRefLike(value: unknown): value is DBRefLike { * @public * @category BSONType */ -export class DBRef { +export class DBRef extends BSONValue { get _bsontype(): 'DBRef' { return 'DBRef'; } @@ -44,6 +45,7 @@ export class DBRef { * @param db - optional db name, if omitted the reference is local to the current db. */ constructor(collection: string, oid: ObjectId, db?: string, fields?: Document) { + super(); // check if namespace has been provided const parts = collection.split('.'); if (parts.length === 2) { diff --git a/src/decimal128.ts b/src/decimal128.ts index e4c3fef8..55543a76 100644 --- a/src/decimal128.ts +++ b/src/decimal128.ts @@ -1,3 +1,4 @@ +import { BSONValue } from './bson_value'; import { BSONError } from './error'; import { Long } from './long'; import { isUint8Array } from './parser/utils'; @@ -126,7 +127,7 @@ export interface Decimal128Extended { * @public * @category BSONType */ -export class Decimal128 { +export class Decimal128 extends BSONValue { get _bsontype(): 'Decimal128' { return 'Decimal128'; } @@ -138,6 +139,7 @@ export class Decimal128 { * or a string representation as returned by .toString() */ constructor(bytes: Uint8Array | string) { + super(); if (typeof bytes === 'string') { this.bytes = Decimal128.fromString(bytes).bytes; } else if (isUint8Array(bytes)) { diff --git a/src/double.ts b/src/double.ts index 43c86be0..45815360 100644 --- a/src/double.ts +++ b/src/double.ts @@ -1,3 +1,4 @@ +import { BSONValue } from './bson_value'; import type { EJSONOptions } from './extended_json'; /** @public */ @@ -10,7 +11,7 @@ export interface DoubleExtended { * @public * @category BSONType */ -export class Double { +export class Double extends BSONValue { get _bsontype(): 'Double' { return 'Double'; } @@ -22,6 +23,7 @@ export class Double { * @param value - the number we want to represent as a double. */ constructor(value: number) { + super(); if ((value as unknown) instanceof Number) { value = value.valueOf(); } diff --git a/src/error.ts b/src/error.ts index 88e16b3e..b01cde53 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,3 +1,5 @@ +import { BSON_MAJOR_VERSION } from './constants'; + /** * @public * `BSONError` objects are thrown when runtime errors occur. @@ -43,3 +45,16 @@ export class BSONError extends Error { ); } } + +/** @public */ +export class BSONVersionError extends BSONError { + get name(): 'BSONVersionError' { + return 'BSONVersionError'; + } + + constructor() { + super( + `Unsupported BSON version, bson types must be from bson ${BSON_MAJOR_VERSION}.0 or later` + ); + } +} diff --git a/src/extended_json.ts b/src/extended_json.ts index d9b5f8f6..e3f07c61 100644 --- a/src/extended_json.ts +++ b/src/extended_json.ts @@ -1,11 +1,17 @@ import { Binary } from './binary'; import type { Document } from './bson'; import { Code } from './code'; -import { BSON_INT32_MAX, BSON_INT32_MIN, BSON_INT64_MAX, BSON_INT64_MIN } from './constants'; +import { + BSON_INT32_MAX, + BSON_INT32_MIN, + BSON_INT64_MAX, + BSON_INT64_MIN, + BSON_MAJOR_VERSION +} from './constants'; import { DBRef, isDBRefLike } from './db_ref'; import { Decimal128 } from './decimal128'; import { Double } from './double'; -import { BSONError } from './error'; +import { BSONError, BSONVersionError } from './error'; import { Int32 } from './int_32'; import { Long } from './long'; import { MaxKey } from './max_key'; @@ -273,13 +279,9 @@ const BSON_TYPE_MAPPINGS = { ), MaxKey: () => new MaxKey(), MinKey: () => new MinKey(), - ObjectID: (o: ObjectId) => new ObjectId(o), - // The _bsontype for ObjectId is spelled with a capital "D", to the mapping above will be used (most of the time) - // specifically BSON versions 4.0.0 and 4.0.1 the _bsontype was changed to "ObjectId" so we keep this mapping to support - // those version of BSON ObjectId: (o: ObjectId) => new ObjectId(o), BSONRegExp: (o: BSONRegExp) => new BSONRegExp(o.pattern, o.options), - Symbol: (o: BSONSymbol) => new BSONSymbol(o.value), + BSONSymbol: (o: BSONSymbol) => new BSONSymbol(o.value), Timestamp: (o: Timestamp) => Timestamp.fromBits(o.low, o.high) } as const; @@ -310,6 +312,13 @@ function serializeDocument(doc: any, options: EJSONSerializeOptions) { } } return _doc; + } else if ( + doc != null && + typeof doc === 'object' && + typeof doc._bsontype === 'string' && + doc[Symbol.for('@@mdb.bson.version')] !== BSON_MAJOR_VERSION + ) { + throw new BSONVersionError(); } else if (isBSONType(doc)) { // the "document" is really just a BSON type object // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/int_32.ts b/src/int_32.ts index 6160b718..c74f3c82 100644 --- a/src/int_32.ts +++ b/src/int_32.ts @@ -1,3 +1,4 @@ +import { BSONValue } from './bson_value'; import type { EJSONOptions } from './extended_json'; /** @public */ @@ -10,7 +11,7 @@ export interface Int32Extended { * @public * @category BSONType */ -export class Int32 { +export class Int32 extends BSONValue { get _bsontype(): 'Int32' { return 'Int32'; } @@ -22,6 +23,7 @@ export class Int32 { * @param value - the number we want to represent as an int32. */ constructor(value: number | string) { + super(); if ((value as unknown) instanceof Number) { value = value.valueOf(); } diff --git a/src/long.ts b/src/long.ts index 65f61f2b..0e6911e1 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1,3 +1,4 @@ +import { BSONValue } from './bson_value'; import { BSONError } from './error'; import type { EJSONOptions } from './extended_json'; import type { Timestamp } from './timestamp'; @@ -99,7 +100,7 @@ export interface LongExtended { * case would often result in infinite recursion. * Common constant values ZERO, ONE, NEG_ONE, etc. are found as static properties on this class. */ -export class Long { +export class Long extends BSONValue { get _bsontype(): 'Long' { return 'Long'; } @@ -138,6 +139,7 @@ export class Long { * @param unsigned - Whether unsigned or not, defaults to signed */ constructor(low: number | bigint | string = 0, high?: number | boolean, unsigned?: boolean) { + super(); if (typeof low === 'bigint') { Object.assign(this, Long.fromBigInt(low, !!high)); } else if (typeof low === 'string') { diff --git a/src/max_key.ts b/src/max_key.ts index 5541a65a..2894f58b 100644 --- a/src/max_key.ts +++ b/src/max_key.ts @@ -1,3 +1,5 @@ +import { BSONValue } from './bson_value'; + /** @public */ export interface MaxKeyExtended { $maxKey: 1; @@ -8,7 +10,7 @@ export interface MaxKeyExtended { * @public * @category BSONType */ -export class MaxKey { +export class MaxKey extends BSONValue { get _bsontype(): 'MaxKey' { return 'MaxKey'; } diff --git a/src/min_key.ts b/src/min_key.ts index 99181449..f72cf26b 100644 --- a/src/min_key.ts +++ b/src/min_key.ts @@ -1,3 +1,5 @@ +import { BSONValue } from './bson_value'; + /** @public */ export interface MinKeyExtended { $minKey: 1; @@ -8,7 +10,7 @@ export interface MinKeyExtended { * @public * @category BSONType */ -export class MinKey { +export class MinKey extends BSONValue { get _bsontype(): 'MinKey' { return 'MinKey'; } diff --git a/src/objectid.ts b/src/objectid.ts index 901c3f20..a0358208 100644 --- a/src/objectid.ts +++ b/src/objectid.ts @@ -1,3 +1,4 @@ +import { BSONValue } from './bson_value'; import { BSONError } from './error'; import { isUint8Array } from './parser/utils'; import { BSONDataView, ByteUtils } from './utils/byte_utils'; @@ -27,9 +28,9 @@ const kId = Symbol('id'); * @public * @category BSONType */ -export class ObjectId { - get _bsontype(): 'ObjectID' { - return 'ObjectID'; +export class ObjectId extends BSONValue { + get _bsontype(): 'ObjectId' { + return 'ObjectId'; } /** @internal */ @@ -48,6 +49,7 @@ export class ObjectId { * @param inputId - Can be a 24 character hex string, 12 byte binary Buffer, or a number. */ constructor(inputId?: string | number | ObjectId | ObjectIdLike | Uint8Array) { + super(); // workingId is set based on type of input and whether valid id exists for the input let workingId; if (typeof inputId === 'object' && inputId && 'id' in inputId) { diff --git a/src/parser/calculate_size.ts b/src/parser/calculate_size.ts index e96bf63e..fd1e4a02 100644 --- a/src/parser/calculate_size.ts +++ b/src/parser/calculate_size.ts @@ -1,5 +1,6 @@ import { Binary } from '../binary'; import type { Document } from '../bson'; +import { BSONVersionError } from '../error'; import * as constants from '../constants'; import { ByteUtils } from '../utils/byte_utils'; import { isAnyArrayBuffer, isDate, isRegExp } from './utils'; @@ -77,9 +78,15 @@ function calculateElement( case 'boolean': return (name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) + (1 + 1); case 'object': - if (value == null || value['_bsontype'] === 'MinKey' || value['_bsontype'] === 'MaxKey') { + if ( + value != null && + typeof value._bsontype === 'string' && + value[Symbol.for('@@mdb.bson.version')] !== constants.BSON_MAJOR_VERSION + ) { + throw new BSONVersionError(); + } else if (value == null || value._bsontype === 'MinKey' || value._bsontype === 'MaxKey') { return (name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) + 1; - } else if (value['_bsontype'] === 'ObjectId' || value['_bsontype'] === 'ObjectID') { + } else if (value._bsontype === 'ObjectId') { return (name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) + (12 + 1); } else if (value instanceof Date || isDate(value)) { return (name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) + (8 + 1); @@ -92,14 +99,14 @@ function calculateElement( (name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) + (1 + 4 + 1) + value.byteLength ); } else if ( - value['_bsontype'] === 'Long' || - value['_bsontype'] === 'Double' || - value['_bsontype'] === 'Timestamp' + value._bsontype === 'Long' || + value._bsontype === 'Double' || + value._bsontype === 'Timestamp' ) { return (name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) + (8 + 1); - } else if (value['_bsontype'] === 'Decimal128') { + } else if (value._bsontype === 'Decimal128') { return (name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) + (16 + 1); - } else if (value['_bsontype'] === 'Code') { + } else if (value._bsontype === 'Code') { // Calculate size depending on the availability of a scope if (value.scope != null && Object.keys(value.scope).length > 0) { return ( @@ -120,7 +127,7 @@ function calculateElement( 1 ); } - } else if (value['_bsontype'] === 'Binary') { + } else if (value._bsontype === 'Binary') { const binary: Binary = value; // Check what kind of subtype we have if (binary.sub_type === Binary.SUBTYPE_BYTE_ARRAY) { @@ -133,7 +140,7 @@ function calculateElement( (name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) + (binary.position + 1 + 4 + 1) ); } - } else if (value['_bsontype'] === 'Symbol') { + } else if (value._bsontype === 'Symbol') { return ( (name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) + ByteUtils.utf8ByteLength(value.value) + @@ -141,7 +148,7 @@ function calculateElement( 1 + 1 ); - } else if (value['_bsontype'] === 'DBRef') { + } else if (value._bsontype === 'DBRef') { // Set up correct object for serialization const ordered_values = Object.assign( { @@ -172,7 +179,7 @@ function calculateElement( (value.multiline ? 1 : 0) + 1 ); - } else if (value['_bsontype'] === 'BSONRegExp') { + } else if (value._bsontype === 'BSONRegExp') { return ( (name != null ? ByteUtils.utf8ByteLength(name) + 1 : 0) + 1 + diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts index 076bd3eb..b59a1b77 100644 --- a/src/parser/serializer.ts +++ b/src/parser/serializer.ts @@ -5,7 +5,7 @@ import * as constants from '../constants'; import type { DBRefLike } from '../db_ref'; import type { Decimal128 } from '../decimal128'; import type { Double } from '../double'; -import { BSONError } from '../error'; +import { BSONError, BSONVersionError } from '../error'; import type { Int32 } from '../int_32'; import { Long } from '../long'; import type { MinKey } from '../min_key'; @@ -690,13 +690,11 @@ export function serializeInto( index = serializeNull(buffer, key, value, index); } else if (value === null) { index = serializeNull(buffer, key, value, index); - } else if (value['_bsontype'] === 'ObjectId' || value['_bsontype'] === 'ObjectID') { - index = serializeObjectId(buffer, key, value, index); } else if (isUint8Array(value)) { index = serializeBuffer(buffer, key, value, index); } else if (value instanceof RegExp || isRegExp(value)) { index = serializeRegExp(buffer, key, value, index); - } else if (typeof value === 'object' && value['_bsontype'] == null) { + } else if (typeof value === 'object' && value._bsontype == null) { index = serializeObject( buffer, key, @@ -708,15 +706,22 @@ export function serializeInto( ignoreUndefined, path ); - } else if (value['_bsontype'] === 'Decimal128') { + } else if ( + typeof value === 'object' && + value[Symbol.for('@@mdb.bson.version')] !== constants.BSON_MAJOR_VERSION + ) { + throw new BSONVersionError(); + } else if (value._bsontype === 'ObjectId') { + index = serializeObjectId(buffer, key, value, index); + } else if (value._bsontype === 'Decimal128') { index = serializeDecimal128(buffer, key, value, index); - } else if (value['_bsontype'] === 'Long' || value['_bsontype'] === 'Timestamp') { + } else if (value._bsontype === 'Long' || value._bsontype === 'Timestamp') { index = serializeLong(buffer, key, value, index); - } else if (value['_bsontype'] === 'Double') { + } else if (value._bsontype === 'Double') { index = serializeDouble(buffer, key, value, index); } else if (typeof value === 'function' && serializeFunctions) { index = serializeFunction(buffer, key, value, index); - } else if (value['_bsontype'] === 'Code') { + } else if (value._bsontype === 'Code') { index = serializeCode( buffer, key, @@ -728,20 +733,20 @@ export function serializeInto( ignoreUndefined, path ); - } else if (value['_bsontype'] === 'Binary') { + } else if (value._bsontype === 'Binary') { index = serializeBinary(buffer, key, value, index); - } else if (value['_bsontype'] === 'Symbol') { + } else if (value._bsontype === 'BSONSymbol') { index = serializeSymbol(buffer, key, value, index); - } else if (value['_bsontype'] === 'DBRef') { + } else if (value._bsontype === 'DBRef') { index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions, path); - } else if (value['_bsontype'] === 'BSONRegExp') { + } else if (value._bsontype === 'BSONRegExp') { index = serializeBSONRegExp(buffer, key, value, index); - } else if (value['_bsontype'] === 'Int32') { + } else if (value._bsontype === 'Int32') { index = serializeInt32(buffer, key, value, index); - } else if (value['_bsontype'] === 'MinKey' || value['_bsontype'] === 'MaxKey') { + } else if (value._bsontype === 'MinKey' || value._bsontype === 'MaxKey') { index = serializeMinMax(buffer, key, value, index); - } else if (typeof value['_bsontype'] !== 'undefined') { - throw new BSONError(`Unrecognized or invalid _bsontype: ${String(value['_bsontype'])}`); + } else if (typeof value._bsontype !== 'undefined') { + throw new BSONError(`Unrecognized or invalid _bsontype: ${String(value._bsontype)}`); } } } else if (object instanceof Map || isMap(object)) { @@ -795,13 +800,11 @@ export function serializeInto( index = serializeDate(buffer, key, value, index); } else if (value === null || (value === undefined && ignoreUndefined === false)) { index = serializeNull(buffer, key, value, index); - } else if (value['_bsontype'] === 'ObjectId' || value['_bsontype'] === 'ObjectID') { - index = serializeObjectId(buffer, key, value, index); } else if (isUint8Array(value)) { index = serializeBuffer(buffer, key, value, index); } else if (value instanceof RegExp || isRegExp(value)) { index = serializeRegExp(buffer, key, value, index); - } else if (type === 'object' && value['_bsontype'] == null) { + } else if (type === 'object' && value._bsontype == null) { index = serializeObject( buffer, key, @@ -813,13 +816,20 @@ export function serializeInto( ignoreUndefined, path ); - } else if (type === 'object' && value['_bsontype'] === 'Decimal128') { + } else if ( + typeof value === 'object' && + value[Symbol.for('@@mdb.bson.version')] !== constants.BSON_MAJOR_VERSION + ) { + throw new BSONVersionError(); + } else if (value._bsontype === 'ObjectId') { + index = serializeObjectId(buffer, key, value, index); + } else if (type === 'object' && value._bsontype === 'Decimal128') { index = serializeDecimal128(buffer, key, value, index); - } else if (value['_bsontype'] === 'Long' || value['_bsontype'] === 'Timestamp') { + } else if (value._bsontype === 'Long' || value._bsontype === 'Timestamp') { index = serializeLong(buffer, key, value, index); - } else if (value['_bsontype'] === 'Double') { + } else if (value._bsontype === 'Double') { index = serializeDouble(buffer, key, value, index); - } else if (value['_bsontype'] === 'Code') { + } else if (value._bsontype === 'Code') { index = serializeCode( buffer, key, @@ -833,20 +843,20 @@ export function serializeInto( ); } else if (typeof value === 'function' && serializeFunctions) { index = serializeFunction(buffer, key, value, index); - } else if (value['_bsontype'] === 'Binary') { + } else if (value._bsontype === 'Binary') { index = serializeBinary(buffer, key, value, index); - } else if (value['_bsontype'] === 'Symbol') { + } else if (value._bsontype === 'BSONSymbol') { index = serializeSymbol(buffer, key, value, index); - } else if (value['_bsontype'] === 'DBRef') { + } else if (value._bsontype === 'DBRef') { index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions, path); - } else if (value['_bsontype'] === 'BSONRegExp') { + } else if (value._bsontype === 'BSONRegExp') { index = serializeBSONRegExp(buffer, key, value, index); - } else if (value['_bsontype'] === 'Int32') { + } else if (value._bsontype === 'Int32') { index = serializeInt32(buffer, key, value, index); - } else if (value['_bsontype'] === 'MinKey' || value['_bsontype'] === 'MaxKey') { + } else if (value._bsontype === 'MinKey' || value._bsontype === 'MaxKey') { index = serializeMinMax(buffer, key, value, index); - } else if (typeof value['_bsontype'] !== 'undefined') { - throw new BSONError(`Unrecognized or invalid _bsontype: ${String(value['_bsontype'])}`); + } else if (typeof value._bsontype !== 'undefined') { + throw new BSONError(`Unrecognized or invalid _bsontype: ${String(value._bsontype)}`); } } } else { @@ -900,13 +910,11 @@ export function serializeInto( if (ignoreUndefined === false) index = serializeNull(buffer, key, value, index); } else if (value === null) { index = serializeNull(buffer, key, value, index); - } else if (value['_bsontype'] === 'ObjectId' || value['_bsontype'] === 'ObjectID') { - index = serializeObjectId(buffer, key, value, index); } else if (isUint8Array(value)) { index = serializeBuffer(buffer, key, value, index); } else if (value instanceof RegExp || isRegExp(value)) { index = serializeRegExp(buffer, key, value, index); - } else if (type === 'object' && value['_bsontype'] == null) { + } else if (type === 'object' && value._bsontype == null) { index = serializeObject( buffer, key, @@ -918,13 +926,20 @@ export function serializeInto( ignoreUndefined, path ); - } else if (type === 'object' && value['_bsontype'] === 'Decimal128') { + } else if ( + typeof value === 'object' && + value[Symbol.for('@@mdb.bson.version')] !== constants.BSON_MAJOR_VERSION + ) { + throw new BSONVersionError(); + } else if (value._bsontype === 'ObjectId') { + index = serializeObjectId(buffer, key, value, index); + } else if (type === 'object' && value._bsontype === 'Decimal128') { index = serializeDecimal128(buffer, key, value, index); - } else if (value['_bsontype'] === 'Long' || value['_bsontype'] === 'Timestamp') { + } else if (value._bsontype === 'Long' || value._bsontype === 'Timestamp') { index = serializeLong(buffer, key, value, index); - } else if (value['_bsontype'] === 'Double') { + } else if (value._bsontype === 'Double') { index = serializeDouble(buffer, key, value, index); - } else if (value['_bsontype'] === 'Code') { + } else if (value._bsontype === 'Code') { index = serializeCode( buffer, key, @@ -938,20 +953,20 @@ export function serializeInto( ); } else if (typeof value === 'function' && serializeFunctions) { index = serializeFunction(buffer, key, value, index); - } else if (value['_bsontype'] === 'Binary') { + } else if (value._bsontype === 'Binary') { index = serializeBinary(buffer, key, value, index); - } else if (value['_bsontype'] === 'Symbol') { + } else if (value._bsontype === 'BSONSymbol') { index = serializeSymbol(buffer, key, value, index); - } else if (value['_bsontype'] === 'DBRef') { + } else if (value._bsontype === 'DBRef') { index = serializeDBRef(buffer, key, value, index, depth, serializeFunctions, path); - } else if (value['_bsontype'] === 'BSONRegExp') { + } else if (value._bsontype === 'BSONRegExp') { index = serializeBSONRegExp(buffer, key, value, index); - } else if (value['_bsontype'] === 'Int32') { + } else if (value._bsontype === 'Int32') { index = serializeInt32(buffer, key, value, index); - } else if (value['_bsontype'] === 'MinKey' || value['_bsontype'] === 'MaxKey') { + } else if (value._bsontype === 'MinKey' || value._bsontype === 'MaxKey') { index = serializeMinMax(buffer, key, value, index); - } else if (typeof value['_bsontype'] !== 'undefined') { - throw new BSONError(`Unrecognized or invalid _bsontype: ${String(value['_bsontype'])}`); + } else if (typeof value._bsontype !== 'undefined') { + throw new BSONError(`Unrecognized or invalid _bsontype: ${String(value._bsontype)}`); } } } diff --git a/src/regexp.ts b/src/regexp.ts index d4dc75a7..0e7b279d 100644 --- a/src/regexp.ts +++ b/src/regexp.ts @@ -1,3 +1,4 @@ +import { BSONValue } from './bson_value'; import { BSONError } from './error'; import type { EJSONOptions } from './extended_json'; @@ -24,7 +25,7 @@ export interface BSONRegExpExtended { * @public * @category BSONType */ -export class BSONRegExp { +export class BSONRegExp extends BSONValue { get _bsontype(): 'BSONRegExp' { return 'BSONRegExp'; } @@ -36,6 +37,7 @@ export class BSONRegExp { * @param options - The regular expression options */ constructor(pattern: string, options?: string) { + super(); this.pattern = pattern; this.options = alphabetize(options ?? ''); @@ -100,4 +102,13 @@ export class BSONRegExp { } throw new BSONError(`Unexpected BSONRegExp EJSON object form: ${JSON.stringify(doc)}`); } + + /** @internal */ + [Symbol.for('nodejs.util.inspect.custom')](): string { + return this.inspect(); + } + + inspect(): string { + return `new BSONRegExp(${JSON.stringify(this.pattern)}, ${JSON.stringify(this.options)})`; + } } diff --git a/src/symbol.ts b/src/symbol.ts index a45554b4..081a52ad 100644 --- a/src/symbol.ts +++ b/src/symbol.ts @@ -1,3 +1,5 @@ +import { BSONValue } from './bson_value'; + /** @public */ export interface BSONSymbolExtended { $symbol: string; @@ -8,9 +10,9 @@ export interface BSONSymbolExtended { * @public * @category BSONType */ -export class BSONSymbol { - get _bsontype(): 'Symbol' { - return 'Symbol'; +export class BSONSymbol extends BSONValue { + get _bsontype(): 'BSONSymbol' { + return 'BSONSymbol'; } value!: string; @@ -18,6 +20,7 @@ export class BSONSymbol { * @param value - the string representing the symbol. */ constructor(value: string) { + super(); this.value = value; } @@ -30,7 +33,6 @@ export class BSONSymbol { return this.value; } - /** @internal */ inspect(): string { return `new BSONSymbol("${this.value}")`; } diff --git a/test/node/bson_older_versions_tests.js b/test/node/bson_older_versions_tests.js deleted file mode 100644 index 3f459269..00000000 --- a/test/node/bson_older_versions_tests.js +++ /dev/null @@ -1,254 +0,0 @@ -'use strict'; - -const currentNodeBSON = require('../register-bson'); -const vm = require('vm'); -const fs = require('fs'); -const rimraf = require('rimraf'); -const cp = require('child_process'); -const { __isWeb__ } = require('../register-bson'); - -// node-fetch is an es-module -let fetch; - -/* - * This file tests that previous versions of BSON - * serialize and deserialize correctly in the most recent version of BSON, - * and that the different distributions (browser, Node.js, etc.) of the - * most recent version are mutually compatible as well. - * - * This is an unusual situation to run into as users should be using one BSON lib version - * but it does arise with sub deps etc. and we wish to protect against unexpected behavior - * - * If backwards compatibility breaks there should be clear warnings/failures - * rather than empty or zero-ed values. - */ -const OLD_VERSIONS = ['v1.1.5', 'v1.1.4']; -const getZipUrl = ver => `https://github.com/mongodb/js-bson/archive/${ver}.zip`; -const getImportPath = ver => `../../bson-${ver}/js-bson-${ver.substring(1)}`; - -function downloadZip(version, done) { - // downloads a zip of previous BSON version - fetch(getZipUrl(version)) - .then(r => { - return r.arrayBuffer(); - }) - .then(r => { - fs.writeFileSync(`bson-${version}.zip`, new Uint8Array(r)); - try { - // unzips the code, right now these test won't handle versions written in TS - cp.execSync(`unzip bson-${version}.zip -d bson-${version}`); - } catch (err) { - return done(err); - } - done(); - }); -} - -// TODO(NODE-4843): These tests are failing not because of an error with BSON but the bundling changes -// Need to be accounted for. We can make the fixes in the ticket focused on this cross compat testing -describe.skip('Mutual version and distribution compatibility', function () { - before(function () { - if (__isWeb__) this.skip(); - }); - - before(async () => { - fetch = await import('node-fetch').then(mod => mod.default); - }); - - OLD_VERSIONS.forEach(version => { - before(function (done) { - this.timeout(30000); // Downloading may take a few seconds. - if (Number(process.version.split('.')[0].substring(1)) < 8) { - // WHATWG fetch doesn't download correctly prior to node 8 - // but we should be safe by testing on node 8 + - return done(); - } - if (fs.existsSync(`bson-${version}.zip`)) { - fs.unlinkSync(`bson-${version}.zip`); - rimraf(`./bson-${version}`, err => { - if (err) done(err); - - // download old versions - downloadZip(version, done); - }); - } else { - // download old versions - downloadZip(version, done); - } - }); - - after(function (done) { - try { - fs.unlinkSync(`bson-${version}.zip`); - } catch (e) { - // ignore - } - rimraf(`./bson-${version}`, err => { - if (err) done(err); - done(); - }); - }); - }); - - // Node.js requires an .mjs filename extension for loading ES modules. - before(() => { - try { - fs.writeFileSync( - './bson.browser.esm.mjs', - fs.readFileSync(__dirname + '/../../dist/bson.browser.esm.js') - ); - fs.writeFileSync('./bson.esm.mjs', fs.readFileSync(__dirname + '/../../dist/bson.esm.js')); - } catch (e) { - // bundling fails in CI on Windows, no idea why, hence also the - // process.platform !== 'win32' check below - } - }); - - after(() => { - try { - fs.unlinkSync('./bson.browser.esm.mjs'); - fs.unlinkSync('./bson.esm.mjs'); - } catch (e) { - // ignore - } - }); - - const variants = OLD_VERSIONS.map(version => ({ - name: `legacy ${version}`, - load: () => { - const bson = require(getImportPath(version)); - bson.serialize = bson.prototype.serialize; - bson.deserialize = bson.prototype.deserialize; - return Promise.resolve(bson); - }, - legacy: true - })).concat([ - { - name: 'Node.js lib/bson', - load: () => Promise.resolve(currentNodeBSON) - }, - { - name: 'Browser ESM', - // eval because import is a syntax error in earlier Node.js versions - // that are still supported in CI - load: () => eval(`import("${__dirname}/../../bson.browser.esm.mjs")`), - usesBufferPolyfill: true - }, - { - name: 'Browser UMD', - load: () => Promise.resolve(require('../../dist/bson.browser.umd.js')), - usesBufferPolyfill: true - }, - { - name: 'Generic bundle', - load: () => { - const source = fs.readFileSync(__dirname + '/../../dist/bson.bundle.js', 'utf8'); - return Promise.resolve(vm.runInNewContext(`${source}; BSON`, { global, process })); - }, - usesBufferPolyfill: true - }, - { - name: 'Node.js ESM', - load: () => eval(`import("${__dirname}/../../bson.esm.mjs")`) - } - ]); - - const makeObjects = bson => [ - new bson.ObjectId('5f16b8bebe434dc98cdfc9ca'), - new bson.DBRef('a', new bson.ObjectId('5f16b8bebe434dc98cdfc9cb'), 'db'), - new bson.MinKey(), - new bson.MaxKey(), - // TODO(NODE-4843): Timestamp has a necessary incompatibility, two number ctor throws now -- new bson.Timestamp(1, 100), - new bson.Timestamp(new bson.Long(1, 100, true)), - new bson.Code('abc'), - bson.Decimal128.fromString('1'), - bson.Long.fromString('1'), - new bson.Binary(Buffer.from('abcäbc🎉'), 128), - new Date('2021-05-04T15:49:33.000Z'), - /match/ - ]; - - for (const from of variants) { - for (const to of variants) { - describe(`serializing objects from ${from.name} using ${to.name}`, () => { - let fromObjects; - let fromBSON; - let toBSON; - - before(function () { - // Load the from/to BSON versions asynchronously because e.g. ESM - // requires asynchronous loading. - return Promise.resolve() - .then(() => { - return from.load(); - }) - .then(loaded => { - fromBSON = loaded; - return to.load(); - }) - .then(loaded => { - toBSON = loaded; - }) - .then( - () => { - if (from.usesBufferPolyfill || to.usesBufferPolyfill) { - // TODO(NODE-3555): The buffer polyfill does not correctly identify ArrayBuffers, will be fixed by removing - return this.skip(); - } - fromObjects = makeObjects(fromBSON); - }, - err => { - if (+process.version.slice(1).split('.')[0] >= 12) { - throw err; // On Node.js 12+, all loading is expected to work. - } else { - this.skip(); // Otherwise, e.g. ESM can't be loaded, so just skip. - } - } - ); - }); - - it('serializes in a compatible way', function () { - for (const object of fromObjects) { - // If the object in question uses Buffers in its serialization, and - // its Buffer was created using the polyfill, and we're serializing - // using a legacy version that uses buf.copy(), then that fails - // because the Buffer polyfill's typechecking is buggy, so we - // skip these cases. - // This is tracked as https://jira.mongodb.org/browse/NODE-2848 - // and would be addressed by https://github.com/feross/buffer/pull/285 - // if that is merged at some point. - if (from.usesBufferPolyfill || to.usesBufferPolyfill) { - // TODO(NODE-3555): The buffer polyfill does not correctly identify ArrayBuffers, will be fixed by removing - return this.skip(); - } - if ( - from.usesBufferPolyfill && - to.legacy && - ['ObjectId', 'Decimal128', 'DBRef', 'Binary'].includes(object.constructor.name) - ) { - continue; - } - - try { - // Check that both BSON versions serialize to equal Buffers - expect(toBSON.serialize({ object })).to.deep.equal(fromBSON.serialize({ object })); - if (!from.legacy) { - // Check that serializing using one version and deserializing using - // the other gives back the original object. - const cloned = fromBSON.deserialize(toBSON.serialize({ object })).object; - expect(fromBSON.EJSON.serialize(cloned)).to.deep.equal( - fromBSON.EJSON.serialize(object) - ); - } - } catch (err) { - // If something fails, note the object type in the error message - // for easier debugging. - err.message += ` (${object.constructor.name})`; - throw err; - } - } - }); - }); - } - } -}); diff --git a/test/node/bson_test.js b/test/node/bson_test.js index ee2c5b9a..eea885d9 100644 --- a/test/node/bson_test.js +++ b/test/node/bson_test.js @@ -1806,88 +1806,10 @@ describe('BSON', function () { done(); }); - it('should serialize ObjectIds from old bson versions', function () { - // In versions 4.0.0 and 4.0.1, we used _bsontype="ObjectId" which broke - // backwards compatibility with mongodb-core and other code. It was reverted - // back to "ObjectID" (capital D) in later library versions. - // The test below ensures that all three versions of Object ID work OK: - // 1. The current version's class - // 2. A simulation of the class from library 4.0.0 - // 3. The class currently in use by mongodb (not tested in browser where mongodb is unavailable) - - // test the old ObjectID class (in mongodb-core 3.1) because MongoDB drivers still return it - function getOldBSON() { - try { - // do a dynamic resolve to avoid exception when running browser tests - const file = require.resolve('mongodb-core'); - const oldModule = require(file).BSON; - const funcs = new oldModule.BSON(); - oldModule.serialize = funcs.serialize; - oldModule.deserialize = funcs.deserialize; - return oldModule; - } catch (e) { - return BSON; // if mongo is unavailable, e.g. browser tests, just re-use new BSON - } - } - - const OldBSON = getOldBSON(); - const OldObjectID = OldBSON === BSON ? BSON.ObjectId : OldBSON.ObjectID; - - // create a wrapper simulating the old ObjectId class from v4.0.0 - class ObjectIdv400 { - constructor() { - this.oid = new ObjectId(); - } - get id() { - return this.oid.id; - } - toString() { - return this.oid.toString(); - } - } - Object.defineProperty(ObjectIdv400.prototype, '_bsontype', { value: 'ObjectId' }); - - // Array - const array = [new ObjectIdv400(), new OldObjectID(), new ObjectId()]; - const deserializedArrayAsMap = BSON.deserialize( - BSON.serialize(Object.fromEntries(array.entries())) - ); - const deserializedArray = Object.keys(deserializedArrayAsMap).map( - x => deserializedArrayAsMap[x] - ); - expect(deserializedArray.map(x => x.toString())).to.eql(array.map(x => x.toString())); - - // Map - const map = new Map(); - map.set('oldBsonType', new ObjectIdv400()); - map.set('reallyOldBsonType', new OldObjectID()); - map.set('newBsonType', new ObjectId()); - const deserializedMapAsObject = BSON.deserialize(BSON.serialize(map), { relaxed: false }); - const deserializedMap = new Map( - Object.keys(deserializedMapAsObject).map(k => [k, deserializedMapAsObject[k]]) - ); - - map.forEach((value, key) => { - expect(deserializedMap.has(key)).to.be.true; - const deserializedMapValue = deserializedMap.get(key); - expect(deserializedMapValue.toString()).to.equal(value.toString()); - }); - - // Object - const record = { - oldBsonType: new ObjectIdv400(), - reallyOldBsonType: new OldObjectID(), - newBsonType: new ObjectId() - }; - const deserializedObject = BSON.deserialize(BSON.serialize(record)); - expect(deserializedObject).to.have.keys(['oldBsonType', 'reallyOldBsonType', 'newBsonType']); - expect(record.oldBsonType.toString()).to.equal(deserializedObject.oldBsonType.toString()); - expect(record.newBsonType.toString()).to.equal(deserializedObject.newBsonType.toString()); - }); - it('should throw if invalid BSON types are input to BSON serializer', function () { const oid = new ObjectId('111111111111111111111111'); - const badBsonType = Object.assign({}, oid, { _bsontype: 'bogus' }); + const badBsonType = new ObjectId('111111111111111111111111'); + Object.defineProperty(badBsonType, '_bsontype', { value: 'bogus' }); const badDoc = { bad: badBsonType }; const badArray = [oid, badDoc]; const badMap = new Map([ @@ -1896,9 +1818,9 @@ describe('BSON', function () { ['c', badArray] ]); - expect(() => BSON.serialize(badDoc)).to.throw(BSONError); - expect(() => BSON.serialize(badArray)).to.throw(BSONError); - expect(() => BSON.serialize(badMap)).to.throw(BSONError); + expect(() => BSON.serialize(badDoc)).to.throw(/invalid _bsontype/); + expect(() => BSON.serialize({ badArray })).to.throw(/invalid _bsontype/); + expect(() => BSON.serialize(badMap)).to.throw(/invalid _bsontype/); }); describe('Should support util.inspect for', function () { diff --git a/test/node/bson_type_classes.test.ts b/test/node/bson_type_classes.test.ts new file mode 100644 index 00000000..8d69d5b5 --- /dev/null +++ b/test/node/bson_type_classes.test.ts @@ -0,0 +1,100 @@ +import { expect } from 'chai'; +import { __isWeb__ } from '../register-bson'; +import { + Binary, + BSONRegExp, + BSONSymbol, + Code, + DBRef, + Decimal128, + Double, + Int32, + Long, + MaxKey, + MinKey, + ObjectId, + Timestamp, + UUID, + BSONValue +} from '../register-bson'; + +const BSONTypeClasses = [ + Binary, + Code, + DBRef, + Decimal128, + Double, + Int32, + Long, + MinKey, + MaxKey, + ObjectId, + BSONRegExp, + BSONSymbol, + Timestamp, + UUID +]; + +const BSONTypeClassCtors = new Map BSONValue>([ + ['Binary', () => new Binary()], + ['Code', () => new Code('function () {}')], + ['DBRef', () => new DBRef('name', new ObjectId('00'.repeat(12)))], + ['Decimal128', () => new Decimal128('1.23')], + ['Double', () => new Double(1.23)], + ['Int32', () => new Int32(1)], + ['Long', () => new Long(1n)], + ['MinKey', () => new MinKey()], + ['MaxKey', () => new MaxKey()], + ['ObjectId', () => new ObjectId('00'.repeat(12))], + ['BSONRegExp', () => new BSONRegExp('abc', 'i')], + ['BSONSymbol', () => new BSONSymbol('name')], + ['Timestamp', () => new Timestamp({ t: 1, i: 2 })], + ['UUID', () => new UUID()] +]); + +describe('BSON Type classes common interfaces', () => { + context('shared inheritance from BSONValue', () => { + before(function () { + if (__isWeb__) { + return this.currentTest?.skip(); + } + }); + for (const [name, creator] of BSONTypeClassCtors) { + it(`${name} inherits from BSONValue`, () => { + expect(creator()).to.be.instanceOf(BSONValue); + }); + } + }); + + for (const TypeClass of BSONTypeClasses) { + describe(TypeClass.name, () => { + if (TypeClass.name !== 'UUID') { + it(`defines a _bsontype property equal to its name`, () => + expect(TypeClass.prototype).to.have.property('_bsontype', TypeClass.name)); + } else { + it(`UUID inherits _bsontype from Binary`, () => + expect(Object.getPrototypeOf(TypeClass.prototype)).to.have.property( + '_bsontype', + 'Binary' + )); + } + + it(`defines a Symbol.for('@@mdb.bson.version') property equal to 5`, () => + expect(TypeClass.prototype).to.have.property(Symbol.for('@@mdb.bson.version'), 5)); + + it(`defines a static fromExtendedJSON() method`, () => + expect(TypeClass).to.have.property('fromExtendedJSON').that.is.a('function')); + + it(`defines a toExtendedJSON() method`, () => + expect(TypeClass.prototype).to.have.property('toExtendedJSON').that.is.a('function')); + + it(`defines an inspect() method`, () => + expect(TypeClass.prototype).to.have.property('inspect').that.is.a('function')); + + it(`defines a [Symbol.for('nodejs.util.inspect.custom')]() method`, () => + expect(TypeClass.prototype) + .to.have.property(Symbol.for('nodejs.util.inspect.custom')) + .that.is.a('function')); + }); + } +}); diff --git a/test/node/cross_compat.test.ts b/test/node/cross_compat.test.ts new file mode 100644 index 00000000..b5abd03d --- /dev/null +++ b/test/node/cross_compat.test.ts @@ -0,0 +1,83 @@ +import { expect } from 'chai'; +import { + BSON, + EJSON, + Binary, + BSONRegExp, + BSONSymbol, + Code, + DBRef, + Decimal128, + Double, + Int32, + Long, + MaxKey, + MinKey, + ObjectId, + Timestamp, + UUID +} from '../register-bson'; + +const BSONTypeClasses = { + Binary: () => { + return new Binary(Buffer.alloc(3)); + }, + Code: () => { + return new Code('function () {}'); + }, + DBRef: () => { + return new DBRef('test', new ObjectId('00'.repeat(12))); + }, + Decimal128: () => { + return new Decimal128('1.28'); + }, + Double: () => { + return new Double(1.28); + }, + Int32: () => { + return new Int32(1); + }, + Long: () => { + return Long.fromNumber(1); + }, + MinKey: () => { + return new MinKey(); + }, + MaxKey: () => { + return new MaxKey(); + }, + ObjectId: () => { + return new ObjectId('00'.repeat(12)); + }, + BSONRegExp: () => { + return new BSONRegExp('abc', 'i'); + }, + BSONSymbol: () => { + return new BSONSymbol('abc'); + }, + Timestamp: () => { + return new Timestamp({ i: 0, t: 1 }); + }, + UUID: () => { + return new UUID('74e65f2f-6fdb-4c56-8785-bddb8ad79ea2'); + } +}; + +describe('Prevent previous major versions from working with BSON v5 serialize and stringify', function () { + for (const [typeName, typeFactory] of Object.entries(BSONTypeClasses)) { + it(`serialize throws if ${typeName} is missing a version symbol`, () => { + const type = typeFactory(); + Object.defineProperty(type, Symbol.for('@@mdb.bson.version'), { value: null }); // set an own property that overrides the getter + expect(() => BSON.serialize({ type })).to.throw(/Unsupported BSON version/); + expect(() => BSON.serialize({ a: [type] })).to.throw(/Unsupported BSON version/); + expect(() => BSON.serialize(new Map([['type', type]]))).to.throw(/Unsupported BSON version/); + }); + + it(`stringify throws if ${typeName} is missing a version symbol`, () => { + const type = typeFactory(); + Object.defineProperty(type, Symbol.for('@@mdb.bson.version'), { value: null }); // set an own property that overrides the getter + expect(() => EJSON.stringify({ type })).to.throw(/Unsupported BSON version/); + expect(() => EJSON.stringify({ a: [type] })).to.throw(/Unsupported BSON version/); + }); + } +}); diff --git a/test/node/error.test.ts b/test/node/error.test.ts index 126e7f2b..8e1e7289 100644 --- a/test/node/error.test.ts +++ b/test/node/error.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { loadESModuleBSON } from '../load_bson'; -import { __isWeb__, BSONError } from '../register-bson'; +import { __isWeb__, BSONError, BSONVersionError } from '../register-bson'; const instanceOfChecksWork = !__isWeb__; @@ -82,4 +82,14 @@ describe('BSONError', function () { expect(bsonErr.name).equals('BSONError'); expect(bsonErr.message).equals('This is a BSONError message'); }); + + describe('class BSONVersionError', () => { + it('is a BSONError instance', () => { + expect(BSONError.isBSONError(new BSONVersionError())).to.be.true; + }); + + it('has a name property equal to "BSONVersionError"', () => { + expect(new BSONVersionError()).to.have.property('name', 'BSONVersionError'); + }); + }); }); diff --git a/test/node/exports.test.ts b/test/node/exports.test.ts index 004bc616..d4d87f74 100644 --- a/test/node/exports.test.ts +++ b/test/node/exports.test.ts @@ -7,6 +7,8 @@ const EXPECTED_EXPORTS = [ '__isWeb__', 'BSONType', + 'BSONValue', + 'BSONVersionError', 'EJSON', 'Code', 'BSONSymbol', diff --git a/test/node/extended_json.test.ts b/test/node/extended_json.test.ts index 3e25c559..51e19fd7 100644 --- a/test/node/extended_json.test.ts +++ b/test/node/extended_json.test.ts @@ -2,7 +2,7 @@ import * as BSON from '../register-bson'; const EJSON = BSON.EJSON; import * as vm from 'node:vm'; import { expect } from 'chai'; -import { BSONError } from '../../src'; +import { BSONVersionError } from '../../src'; // BSON types const Binary = BSON.Binary; @@ -15,36 +15,11 @@ const Int32 = BSON.Int32; const Long = BSON.Long; const MaxKey = BSON.MaxKey; const MinKey = BSON.MinKey; -const ObjectID = BSON.ObjectId; const ObjectId = BSON.ObjectId; const BSONRegExp = BSON.BSONRegExp; const BSONSymbol = BSON.BSONSymbol; const Timestamp = BSON.Timestamp; -// Several tests in this file can test interop between current library versions and library version 1.1.0, because -// between 1.1.0 and 4.0.0 there was a significant rewrite. To minimize maintenance issues of a hard dependency on -// the old version, these interop tests are inactive by default. To activate, edit the check:test script in package.json: -// "check:test": "npm i --no-save --force bson@1.1.0 && mocha ./test/node && npm uninstall --no-save --force bson@1.1.0" -// -function getOldBSON() { - try { - // do a dynamic resolve to avoid exception when running browser tests - const file = require.resolve('bson'); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const oldModule = require(file).BSON; - const funcs = new oldModule.BSON(); - oldModule.serialize = funcs.serialize; - oldModule.deserialize = funcs.deserialize; - return oldModule; - } catch (e) { - return BSON; // if old bson lib is unavailable, e.g. browser tests, just re-use new BSON - } -} - -const OldBSON = getOldBSON(); -const OldObjectID = OldBSON === BSON ? BSON.ObjectId : OldBSON.ObjectID; -const usingOldBSON = OldBSON !== BSON; - describe('Extended JSON', function () { let doc = {}; @@ -67,8 +42,6 @@ describe('Extended JSON', function () { maxKey: new MaxKey(), minKey: new MinKey(), objectId: ObjectId.createFromHexString('111111111111111111111111'), - objectID: ObjectID.createFromHexString('111111111111111111111111'), - oldObjectID: OldObjectID.createFromHexString('111111111111111111111111'), regexp: new BSONRegExp('hello world', 'i'), symbol: new BSONSymbol('symbol'), timestamp: Timestamp.fromNumber(1000), @@ -82,7 +55,7 @@ describe('Extended JSON', function () { it('should correctly extend an existing mongodb module', function () { // TODO(NODE-4377): doubleNumberIntFit should be a double not a $numberLong const json = - '{"_id":{"$numberInt":"100"},"gh":{"$numberInt":"1"},"binary":{"$binary":{"base64":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==","subType":"00"}},"date":{"$date":{"$numberLong":"1488372056737"}},"code":{"$code":"function() {}","$scope":{"a":{"$numberInt":"1"}}},"dbRef":{"$ref":"tests","$id":{"$numberInt":"1"},"$db":"test"},"decimal":{"$numberDecimal":"100"},"double":{"$numberDouble":"10.1"},"int32":{"$numberInt":"10"},"long":{"$numberLong":"200"},"maxKey":{"$maxKey":1},"minKey":{"$minKey":1},"objectId":{"$oid":"111111111111111111111111"},"objectID":{"$oid":"111111111111111111111111"},"oldObjectID":{"$oid":"111111111111111111111111"},"regexp":{"$regularExpression":{"pattern":"hello world","options":"i"}},"symbol":{"$symbol":"symbol"},"timestamp":{"$timestamp":{"t":0,"i":1000}},"int32Number":{"$numberInt":"300"},"doubleNumber":{"$numberDouble":"200.2"},"longNumberIntFit":{"$numberLong":"7036874417766400"},"doubleNumberIntFit":{"$numberLong":"19007199250000000"}}'; + '{"_id":{"$numberInt":"100"},"gh":{"$numberInt":"1"},"binary":{"$binary":{"base64":"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==","subType":"00"}},"date":{"$date":{"$numberLong":"1488372056737"}},"code":{"$code":"function() {}","$scope":{"a":{"$numberInt":"1"}}},"dbRef":{"$ref":"tests","$id":{"$numberInt":"1"},"$db":"test"},"decimal":{"$numberDecimal":"100"},"double":{"$numberDouble":"10.1"},"int32":{"$numberInt":"10"},"long":{"$numberLong":"200"},"maxKey":{"$maxKey":1},"minKey":{"$minKey":1},"objectId":{"$oid":"111111111111111111111111"},"regexp":{"$regularExpression":{"pattern":"hello world","options":"i"}},"symbol":{"$symbol":"symbol"},"timestamp":{"$timestamp":{"t":0,"i":1000}},"int32Number":{"$numberInt":"300"},"doubleNumber":{"$numberDouble":"200.2"},"longNumberIntFit":{"$numberLong":"7036874417766400"},"doubleNumberIntFit":{"$numberLong":"19007199250000000"}}'; expect(json).to.equal(EJSON.stringify(doc, null, 0, { relaxed: false })); }); @@ -129,10 +102,6 @@ describe('Extended JSON', function () { it('should correctly serialize bson types when they are values', function () { let serialized = EJSON.stringify(new ObjectId('591801a468f9e7024b6235ea'), { relaxed: false }); expect(serialized).to.equal('{"$oid":"591801a468f9e7024b6235ea"}'); - serialized = EJSON.stringify(new ObjectID('591801a468f9e7024b6235ea'), { relaxed: false }); - expect(serialized).to.equal('{"$oid":"591801a468f9e7024b6235ea"}'); - serialized = EJSON.stringify(new OldObjectID('591801a468f9e7024b6235ea'), { relaxed: false }); - expect(serialized).to.equal('{"$oid":"591801a468f9e7024b6235ea"}'); serialized = EJSON.stringify(new Int32(42), { relaxed: false }); expect(serialized).to.equal('{"$numberInt":"42"}'); @@ -143,20 +112,6 @@ describe('Extended JSON', function () { { relaxed: false } ); expect(serialized).to.equal('{"_id":{"$nin":[{"$oid":"591801a468f9e7024b6235ea"}]}}'); - serialized = EJSON.stringify( - { - _id: { $nin: [new ObjectID('591801a468f9e7024b6235ea')] } - }, - { relaxed: false } - ); - expect(serialized).to.equal('{"_id":{"$nin":[{"$oid":"591801a468f9e7024b6235ea"}]}}'); - serialized = EJSON.stringify( - { - _id: { $nin: [new OldObjectID('591801a468f9e7024b6235ea')] } - }, - { relaxed: false } - ); - expect(serialized).to.equal('{"_id":{"$nin":[{"$oid":"591801a468f9e7024b6235ea"}]}}'); serialized = EJSON.stringify(new Binary(new Uint8Array([1, 2, 3, 4, 5])), { relaxed: false }); expect(serialized).to.equal('{"$binary":{"base64":"AQIDBAU=","subType":"00"}}'); @@ -238,8 +193,6 @@ describe('Extended JSON', function () { maxKey: new MaxKey(), minKey: new MinKey(), objectId: ObjectId.createFromHexString('111111111111111111111111'), - objectID: ObjectID.createFromHexString('111111111111111111111111'), - oldObjectID: OldObjectID.createFromHexString('111111111111111111111111'), bsonRegExp: new BSONRegExp('hello world', 'i'), symbol: new BSONSymbol('symbol'), timestamp: new Timestamp(), @@ -259,8 +212,6 @@ describe('Extended JSON', function () { maxKey: { $maxKey: 1 }, minKey: { $minKey: 1 }, objectId: { $oid: '111111111111111111111111' }, - objectID: { $oid: '111111111111111111111111' }, - oldObjectID: { $oid: '111111111111111111111111' }, bsonRegExp: { $regularExpression: { pattern: 'hello world', options: 'i' } }, symbol: { $symbol: 'symbol' }, timestamp: { $timestamp: { t: 0, i: 0 } }, @@ -281,8 +232,6 @@ describe('Extended JSON', function () { maxKey: { $maxKey: 1 }, minKey: { $minKey: 1 }, objectId: { $oid: '111111111111111111111111' }, - objectID: { $oid: '111111111111111111111111' }, - oldObjectID: { $oid: '111111111111111111111111' }, bsonRegExp: { $regularExpression: { pattern: 'hello world', options: 'i' } }, symbol: { $symbol: 'symbol' }, timestamp: { $timestamp: { t: 0, i: 0 } } @@ -315,8 +264,6 @@ describe('Extended JSON', function () { expect(result.minKey).to.be.an.instanceOf(BSON.MinKey); // objectID expect(result.objectId.toString()).to.equal('111111111111111111111111'); - expect(result.objectID.toString()).to.equal('111111111111111111111111'); - expect(result.oldObjectID.toString()).to.equal('111111111111111111111111'); //bsonRegExp expect(result.bsonRegExp).to.be.an.instanceOf(BSON.BSONRegExp); expect(result.bsonRegExp.pattern).to.equal('hello world'); @@ -353,167 +300,16 @@ describe('Extended JSON', function () { expect(serialized).to.equal('{"a":10}'); }); - if (!usingOldBSON) { - it.skip('skipping 4.x/1.x interop tests', () => { - // ignore - }); - } else { - it('should interoperate 4.x with 1.x versions of this library', function () { - const buffer = Buffer.alloc(64); - for (let i = 0; i < buffer.length; i++) { - buffer[i] = i; - } - const [oldBsonObject, newBsonObject] = [OldBSON, BSON].map(bsonModule => { - const bsonTypes = { - binary: new bsonModule.Binary(buffer), - code: new bsonModule.Code('function() {}'), - dbRef: new bsonModule.DBRef('tests', new Int32(1), 'test'), - decimal128: bsonModule.Decimal128.fromString('9991223372036854775807'), - double: new bsonModule.Double(10.1), - int32: new bsonModule.Int32(10), - long: bsonModule.Long.fromString('1223372036854775807'), - maxKey: new bsonModule.MaxKey(), - // minKey: new bsonModule.MinKey(), // broken until #310 is fixed in 1.x - objectId: bsonModule.ObjectId.createFromHexString('111111111111111111111111'), - objectID: bsonModule.ObjectId.createFromHexString('111111111111111111111111'), - bsonRegExp: new bsonModule.BSONRegExp('hello world', 'i'), - symbol: bsonModule.BSONSymbol - ? new bsonModule.BSONSymbol('symbol') - : new bsonModule.Symbol('symbol'), - timestamp: new bsonModule.Timestamp() - }; - return bsonTypes; - }); - - const serializationOptions = {}; - const bsonBuffers = { - oldObjectOldSerializer: OldBSON.serialize(oldBsonObject, serializationOptions), - oldObjectNewSerializer: BSON.serialize(oldBsonObject, serializationOptions), - newObjectOldSerializer: OldBSON.serialize(newBsonObject, serializationOptions), - newObjectNewSerializer: BSON.serialize(newBsonObject, serializationOptions) - }; - - const expectedBufferBase64 = - 'VgEAAAViaW5hcnkAQAAAAAAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/DWNvZGUADgAAAGZ1bmN0aW9uKCkge30AA2RiUmVmACwAAAACJHJlZgAGAAAAdGVzdHMAECRpZAABAAAAAiRkYgAFAAAAdGVzdAAAE2RlY2ltYWwxMjgA//837RjxE6AdAgAAAABAMAFkb3VibGUAMzMzMzMzJEAQaW50MzIACgAAABJsb25nAP//38RiSvoQf21heEtleQAHb2JqZWN0SWQAERERERERERERERERB29iamVjdElEABEREREREREREREREQtic29uUmVnRXhwAGhlbGxvIHdvcmxkAGkADnN5bWJvbAAHAAAAc3ltYm9sABF0aW1lc3RhbXAAAAAAAAAAAAAA'; - const expectedBuffer = Buffer.from(expectedBufferBase64, 'base64'); - - // Regardless of which library version created the objects, and which library version - // is being used to serialize the objects, validate that the correct BSON is returned. - expect(expectedBuffer).to.deep.equal(bsonBuffers.newObjectNewSerializer); - expect(expectedBuffer).to.deep.equal(bsonBuffers.newObjectOldSerializer); - expect(expectedBuffer).to.deep.equal(bsonBuffers.oldObjectNewSerializer); - expect(expectedBuffer).to.deep.equal(bsonBuffers.oldObjectOldSerializer); - - // Finally, validate that the BSON buffer above is correctly deserialized back to EJSON by the new library, - // regardless of which library version's deserializer is used. This is useful because the 1.x deserializer - // generates 1.x objects, while the 4.x serializer generates 4.x objects. The 4.x EJSON serializer should - // be able to handle both. - const deserializationOptions = { promoteValues: false }; - const deserialized = { - usingOldDeserializer: OldBSON.deserialize(expectedBuffer, deserializationOptions), - usingNewDeserializer: BSON.deserialize(expectedBuffer, deserializationOptions) - }; - // Apparently the Symbol BSON type was deprecated in V4. Symbols in BSON are deserialized as strings in V4 - // Therefore, for this type we know there will be a difference between the V1 library and the V4 library, - // so remove Symbol from the list of BSON types that are being compared. - // Browser tests currently don't handle BSON Symbol correctly, so only test this under Node where OldBSON !=== BSON module. - if (BSON !== OldBSON) { - expect(deserialized.usingOldDeserializer['symbol'].value).to.equal( - deserialized.usingNewDeserializer['symbol'] - ); - } - delete deserialized.usingOldDeserializer['symbol']; - delete deserialized.usingNewDeserializer['symbol']; - - const ejsonExpected = { - binary: { - $binary: { - base64: - 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==', - subType: '00' - } - }, - code: { $code: 'function() {}' }, - dbRef: { $ref: 'tests', $id: { $numberInt: '1' }, $db: 'test' }, - decimal128: { $numberDecimal: '9991223372036854775807' }, - double: { $numberDouble: '10.1' }, - int32: { $numberInt: '10' }, - long: { $numberLong: '1223372036854775807' }, - maxKey: { $maxKey: 1 }, - // minKey: { $minKey: 1 }, // broken until #310 is fixed in 1.x branch - objectId: { $oid: '111111111111111111111111' }, - objectID: { $oid: '111111111111111111111111' }, - bsonRegExp: { $regularExpression: { pattern: 'hello world', options: 'i' } }, - // symbol: { $symbol: 'symbol' }, // removed because this type is deprecated. See comment above. - timestamp: { $timestamp: { t: 0, i: 0 } } - }; - const ejsonSerializeOptions = { relaxed: false }; - const resultOld = EJSON.serialize(deserialized.usingOldDeserializer, ejsonSerializeOptions); - expect(resultOld).to.deep.equal(ejsonExpected); - const resultNew = EJSON.serialize(deserialized.usingNewDeserializer, ejsonSerializeOptions); - expect(resultNew).to.deep.equal(ejsonExpected); - }); - - // Must special-case the test for MinKey, because of #310. When #310 is fixed and is picked up - // by mongodb-core, then remove this test case and uncomment the MinKey checks in the test case above - it('should interop with MinKey 1.x and 4.x, except the case that #310 breaks', function () { - if (!usingOldBSON) { - it.skip('interop tests', () => { - // ignore - }); - return; - } - - const serializationOptions = {}; - const deserializationOptions = { promoteValues: false }; - - // when #310 is fixed and the fix makes it into mongodb-core. - const [oldMinKey, newMinKey] = [OldBSON, BSON].map(bsonModule => { - const bsonTypes = { - minKey: new bsonModule.MinKey() - }; - return bsonTypes; - }); - - const expectedBufferBase64MinKey = 'DQAAAP9taW5LZXkAAA=='; - const expectedBufferMinKey = Buffer.from(expectedBufferBase64MinKey, 'base64'); - - const bsonBuffersMinKey = { - oldObjectOldSerializer: OldBSON.serialize(oldMinKey, serializationOptions), - oldObjectNewSerializer: BSON.serialize(oldMinKey, serializationOptions), - newObjectOldSerializer: OldBSON.serialize(newMinKey, serializationOptions), - newObjectNewSerializer: BSON.serialize(newMinKey, serializationOptions) - }; - - expect(expectedBufferMinKey).to.deep.equal(bsonBuffersMinKey.newObjectNewSerializer); - // expect(expectedBufferMinKey).to.deep.equal(bsonBuffersMinKey.newObjectOldSerializer); // this is the case that's broken by #310 - expect(expectedBufferMinKey).to.deep.equal(bsonBuffersMinKey.oldObjectNewSerializer); - expect(expectedBufferMinKey).to.deep.equal(bsonBuffersMinKey.oldObjectOldSerializer); - - const ejsonExpected = { - minKey: { $minKey: 1 } - }; - - const deserialized = { - usingOldDeserializer: OldBSON.deserialize(expectedBufferMinKey, deserializationOptions), - usingNewDeserializer: BSON.deserialize(expectedBufferMinKey, deserializationOptions) - }; - const ejsonSerializeOptions = { relaxed: false }; - const resultOld = EJSON.serialize(deserialized.usingOldDeserializer, ejsonSerializeOptions); - expect(resultOld).to.deep.equal(ejsonExpected); - const resultNew = EJSON.serialize(deserialized.usingNewDeserializer, ejsonSerializeOptions); - expect(resultNew).to.deep.equal(ejsonExpected); - }); - } - - it('should throw if invalid BSON types are input to EJSON serializer', function () { + it.skip('should throw if invalid BSON types are input to EJSON serializer', function () { + // TODO(NODE-4952): Increase EJSON strictness w.r.t _bsontype validation const oid = new ObjectId('111111111111111111111111'); - const badBsonType = Object.assign({}, oid, { _bsontype: 'bogus' }); + const badBsonType = new ObjectId('111111111111111111111111'); + Object.defineProperty(badBsonType, '_bsontype', { value: 'bogus' }); const badDoc = { bad: badBsonType }; const badArray = [oid, badDoc]; // const badMap = new Map([['a', badBsonType], ['b', badDoc], ['c', badArray]]); - expect(() => EJSON.serialize(badDoc)).to.throw(BSONError); - expect(() => EJSON.serialize(badArray)).to.throw(BSONError); + expect(() => EJSON.serialize(badDoc)).to.throw(/invalid _bsontype/); + expect(() => EJSON.serialize({ badArray })).to.throw(/invalid _bsontype/); // expect(() => EJSON.serialize(badMap)).to.throw(); // uncomment when EJSON supports ES6 Map }); @@ -756,4 +552,12 @@ describe('Extended JSON', function () { const result = JSON.parse(string); expect(result).to.deep.equal({ a: 1 }); }); + + it(`throws if Symbol.for('@@mdb.bson.version') is the wrong version in EJSON.stringify`, () => { + expect(() => + EJSON.stringify({ + a: { _bsontype: 'Int32', value: 2, [Symbol.for('@@mdb.bson.version')]: 1 } + }) + ).to.throw(BSONVersionError, /Unsupported BSON version/i); + }); }); diff --git a/test/node/parser/calculate_size.test.ts b/test/node/parser/calculate_size.test.ts index 0afda66c..3ebccd16 100644 --- a/test/node/parser/calculate_size.test.ts +++ b/test/node/parser/calculate_size.test.ts @@ -1,5 +1,6 @@ import * as BSON from '../../register-bson'; import { expect } from 'chai'; +import { BSONVersionError } from '../../register-bson'; describe('calculateSize()', () => { it('should only enumerate own property keys from input objects', () => { @@ -7,4 +8,20 @@ describe('calculateSize()', () => { Object.setPrototypeOf(input, { b: 2 }); expect(BSON.calculateObjectSize(input)).to.equal(12); }); + + it(`throws if Symbol.for('@@mdb.bson.version') is the wrong version`, () => { + expect(() => + BSON.calculateObjectSize({ + a: { _bsontype: 'Int32', value: 2, [Symbol.for('@@mdb.bson.version')]: 1 } + }) + ).to.throw(BSONVersionError, /Unsupported BSON version/i); + }); + + it(`throws if Symbol.for('@@mdb.bson.version') is not defined`, () => { + expect(() => + BSON.calculateObjectSize({ + a: { _bsontype: 'Int32', value: 2 } + }) + ).to.throw(BSONVersionError, /Unsupported BSON version/i); + }); }); diff --git a/test/node/parser/serializer.test.ts b/test/node/parser/serializer.test.ts index 352fc538..d09c11b1 100644 --- a/test/node/parser/serializer.test.ts +++ b/test/node/parser/serializer.test.ts @@ -1,6 +1,7 @@ import * as BSON from '../../register-bson'; import { bufferFromHexArray } from '../tools/utils'; import { expect } from 'chai'; +import { BSONVersionError } from '../../register-bson'; describe('serialize()', () => { it('should only enumerate own property keys from input objects', () => { @@ -44,9 +45,11 @@ describe('serialize()', () => { it('does not permit objects with a _bsontype string to be serialized at the root', () => { expect(() => BSON.serialize({ _bsontype: 'iLoveJavascript' })).to.throw(/BSON types cannot/); // a nested invalid _bsontype throws something different - expect(() => BSON.serialize({ a: { _bsontype: 'iLoveJavascript' } })).to.throw( - /invalid _bsontype/ - ); + expect(() => + BSON.serialize({ + a: { _bsontype: 'iLoveJavascript', [Symbol.for('@@mdb.bson.version')]: 5 } + }) + ).to.throw(/invalid _bsontype/); }); it('does permit objects with a _bsontype prop that is not a string', () => { @@ -88,5 +91,21 @@ describe('serialize()', () => { expect(() => BSON.serialize(Buffer.alloc(2))).to.throw(/cannot be BSON documents/); expect(() => BSON.serialize(new Uint8Array(3))).to.throw(/cannot be BSON documents/); }); + + it(`throws if Symbol.for('@@mdb.bson.version') is the wrong version`, () => { + expect(() => + BSON.serialize({ + a: { _bsontype: 'Int32', value: 2, [Symbol.for('@@mdb.bson.version')]: 1 } + }) + ).to.throw(BSONVersionError, /Unsupported BSON version/i); + }); + + it(`throws if Symbol.for('@@mdb.bson.version') is not defined`, () => { + expect(() => + BSON.serialize({ + a: { _bsontype: 'Int32', value: 2 } + }) + ).to.throw(BSONVersionError, /Unsupported BSON version/i); + }); }); }); diff --git a/test/node/symbol.test.ts b/test/node/symbol.test.ts new file mode 100644 index 00000000..c63ad859 --- /dev/null +++ b/test/node/symbol.test.ts @@ -0,0 +1,44 @@ +import { expect } from 'chai'; +import { BSONSymbol, BSON } from '../register-bson'; +import { bufferFromHexArray } from './tools/utils'; + +describe('class BSONSymbol', () => { + it('get _bsontype returns BSONSymbol', () => { + const sym = new BSONSymbol('symbol'); + expect(sym).to.have.property('_bsontype', 'BSONSymbol'); + }); + + it('serializes to a bson symbol type', () => { + const bytes = bufferFromHexArray([ + '0E', // bson symbol + Buffer.from('sym\x00', 'utf8').toString('hex'), + '07000000', // 6 bytes + Buffer.from('symbol\x00').toString('hex') + ]); + + expect(BSON.serialize({ sym: new BSONSymbol('symbol') })).to.deep.equal(bytes); + }); + + it('deserializes to js string by default', () => { + const bytes = bufferFromHexArray([ + '0E', // bson symbol + Buffer.from('sym\x00', 'utf8').toString('hex'), + '07000000', // 6 bytes + Buffer.from('symbol\x00').toString('hex') + ]); + + expect(BSON.deserialize(bytes)).to.have.property('sym', 'symbol'); + }); + + it('deserializes to BSONSymbol if promoteValues is false', () => { + const bytes = bufferFromHexArray([ + '0E', // bson symbol + Buffer.from('sym\x00', 'utf8').toString('hex'), + '07000000', // 6 bytes + Buffer.from('symbol\x00').toString('hex') + ]); + + const result = BSON.deserialize(bytes, { promoteValues: false }); + expect(result).to.have.nested.property('sym._bsontype', 'BSONSymbol'); + }); +}); diff --git a/test/node/type_identifier_tests.js b/test/node/type_identifier_tests.js index f23308d1..542bb921 100644 --- a/test/node/type_identifier_tests.js +++ b/test/node/type_identifier_tests.js @@ -18,12 +18,11 @@ const { } = require('../register-bson'); describe('_bsontype identifier', () => { - // The two out of the norm types: - it('should be equal to ObjectID for ObjectId', () => { - expect(ObjectId.prototype._bsontype).to.equal('ObjectID'); + it('should be equal to ObjectId for ObjectId', () => { + expect(ObjectId.prototype._bsontype).to.equal('ObjectId'); }); - it('should be equal to Symbol for BSONSymbol', () => { - expect(BSONSymbol.prototype._bsontype).to.equal('Symbol'); + it('should be equal to BSONSymbol for BSONSymbol', () => { + expect(BSONSymbol.prototype._bsontype).to.equal('BSONSymbol'); }); it('should be equal to Timestamp for Timestamp', () => { // TODO(NODE-2624): Make Timestamp hold its long value on a property rather than be a subclass diff --git a/test/types/bson.test-d.ts b/test/types/bson.test-d.ts index efa3e66d..afa89b91 100644 --- a/test/types/bson.test-d.ts +++ b/test/types/bson.test-d.ts @@ -16,7 +16,8 @@ import { UUID, DBRefLike, Document, - Decimal128Extended + Decimal128Extended, + BSONValue } from '../../bson'; // import from generated bson.d.ts expectType<() => UUID>(Binary.prototype.toUUID); @@ -51,18 +52,14 @@ expectError(MinKey.prototype.toJSON); expectError(Long.prototype.toJSON); expectError(BSONRegExp.prototype.toJSON); -// ObjectID uses a capital "D", this does not relate to the class name, or export name, only the determination for serialization -expectType<'ObjectID'>(ObjectId.prototype._bsontype) -// BSONSymbol was renamed to not conflict with the global JS Symbol -// but its _bsontype is still 'Symbol' -expectType<'Symbol'>(BSONSymbol.prototype._bsontype) - // We hack TS to say that the prototype has _bsontype='Timestamp' // but it actually is _bsontype='Long', inside the Timestamp constructor // we override the property on the instance // TODO(NODE-2624): Make Timestamp hold its long value on a property rather than be a subclass expectType<'Timestamp'>(Timestamp.prototype._bsontype) +expectType<'ObjectId'>(ObjectId.prototype._bsontype) +expectType<'BSONSymbol'>(BSONSymbol.prototype._bsontype) expectType<'Binary'>(Binary.prototype._bsontype) expectType<'Code'>(Code.prototype._bsontype) expectType<'DBRef'>(DBRef.prototype._bsontype) @@ -74,3 +71,8 @@ expectType<'MaxKey'>(MaxKey.prototype._bsontype) expectType<'MinKey'>(MinKey.prototype._bsontype) expectType<'BSONRegExp'>(BSONRegExp.prototype._bsontype) expectType<'Binary'>(UUID.prototype._bsontype) + +// Common BSONValue interface +declare const bsonValue: BSONValue; +expectType(bsonValue._bsontype); +expectType<() => string>(bsonValue.inspect);