From e9e40a2cceb6392065a4f217d17fc4211aa1bbd7 Mon Sep 17 00:00:00 2001 From: Warren James <28974128+W-A-James@users.noreply.github.com> Date: Wed, 4 Jan 2023 09:57:35 -0500 Subject: [PATCH 1/4] feat(NODE-4870): Support BigInt serialization (#541) --- src/parser/serializer.ts | 32 +++++--- test/node/bigint.test.ts | 166 ++++++++++++++++++++++++++++++++++++++ test/node/bigint_tests.js | 72 ----------------- test/node/long.test.ts | 24 ++++++ test/node/long_tests.js | 18 ----- 5 files changed, 209 insertions(+), 103 deletions(-) create mode 100644 test/node/bigint.test.ts delete mode 100644 test/node/bigint_tests.js create mode 100644 test/node/long.test.ts delete mode 100644 test/node/long_tests.js diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts index 91e901da..baf3fe9b 100644 --- a/src/parser/serializer.ts +++ b/src/parser/serializer.ts @@ -12,15 +12,7 @@ import type { MinKey } from '../min_key'; import type { ObjectId } from '../objectid'; import type { BSONRegExp } from '../regexp'; import { ByteUtils } from '../utils/byte_utils'; -import { - isAnyArrayBuffer, - isBigInt64Array, - isBigUInt64Array, - isDate, - isMap, - isRegExp, - isUint8Array -} from './utils'; +import { isAnyArrayBuffer, isDate, isMap, isRegExp, isUint8Array } from './utils'; /** @public */ export interface SerializeOptions { @@ -103,6 +95,20 @@ function serializeNumber(buffer: Uint8Array, key: string, value: number, index: return index; } +function serializeBigInt(buffer: Uint8Array, key: string, value: bigint, index: number) { + buffer[index++] = constants.BSON_DATA_LONG; + // Number of written bytes + const numberOfWrittenBytes = ByteUtils.encodeUTF8Into(buffer, key, index); + // Encode the name + index += numberOfWrittenBytes; + buffer[index++] = 0; + NUMBER_SPACE.setBigInt64(0, value, true); + // Write BigInt value + buffer.set(EIGHT_BYTE_VIEW_ON_NUMBER, index); + index += EIGHT_BYTE_VIEW_ON_NUMBER.byteLength; + return index; +} + function serializeNull(buffer: Uint8Array, key: string, _: unknown, index: number) { // Set long type buffer[index++] = constants.BSON_DATA_NULL; @@ -675,7 +681,7 @@ export function serializeInto( } else if (typeof value === 'number') { index = serializeNumber(buffer, key, value, index); } else if (typeof value === 'bigint') { - throw new BSONError('Unsupported type BigInt, please use Decimal128'); + index = serializeBigInt(buffer, key, value, index); } else if (typeof value === 'boolean') { index = serializeBoolean(buffer, key, value, index); } else if (value instanceof Date || isDate(value)) { @@ -777,8 +783,8 @@ export function serializeInto( index = serializeString(buffer, key, value, index); } else if (type === 'number') { index = serializeNumber(buffer, key, value, index); - } else if (type === 'bigint' || isBigInt64Array(value) || isBigUInt64Array(value)) { - throw new BSONError('Unsupported type BigInt, please use Decimal128'); + } else if (type === 'bigint') { + index = serializeBigInt(buffer, key, value, index); } else if (type === 'boolean') { index = serializeBoolean(buffer, key, value, index); } else if (value instanceof Date || isDate(value)) { @@ -881,7 +887,7 @@ export function serializeInto( } else if (type === 'number') { index = serializeNumber(buffer, key, value, index); } else if (type === 'bigint') { - throw new BSONError('Unsupported type BigInt, please use Decimal128'); + index = serializeBigInt(buffer, key, value, index); } else if (type === 'boolean') { index = serializeBoolean(buffer, key, value, index); } else if (value instanceof Date || isDate(value)) { diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts new file mode 100644 index 00000000..fe771ce3 --- /dev/null +++ b/test/node/bigint.test.ts @@ -0,0 +1,166 @@ +import { BSON } from '../register-bson'; +import { bufferFromHexArray } from './tools/utils'; +import { BSON_DATA_LONG } from '../../src/constants'; +import { BSONDataView } from '../../src/utils/byte_utils'; + +describe('BSON BigInt serialization Support', function () { + // Index for the data type byte of a BSON document with a + // NOTE: These offsets only apply for documents with the shape {a : } + // where n is a BigInt + type SerializedDocParts = { + dataType: number; + key: string; + value: bigint; + }; + /** + * NOTE: this function operates on serialized BSON documents with the shape { : } + * where n is some int64. This function assumes that keys are properly encoded + * with the necessary null byte at the end and only at the end of the key string + */ + function getSerializedDocParts(serializedDoc: Uint8Array): SerializedDocParts { + const DATA_TYPE_OFFSET = 4; + const KEY_OFFSET = 5; + + const dataView = BSONDataView.fromUint8Array(serializedDoc); + const keySlice = serializedDoc.slice(KEY_OFFSET); + + let keyLength = 0; + while (keySlice[keyLength++] !== 0); + + const valueOffset = KEY_OFFSET + keyLength; + const key = Buffer.from(serializedDoc.slice(KEY_OFFSET, KEY_OFFSET + keyLength)).toString( + 'utf8' + ); + + return { + dataType: dataView.getInt8(DATA_TYPE_OFFSET), + key: key.slice(0, keyLength - 1), + value: dataView.getBigInt64(valueOffset, true) + }; + } + + it('serializes bigints with the correct BSON type', function () { + const testDoc = { a: 0n }; + const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); + expect(serializedDoc.dataType).to.equal(BSON_DATA_LONG); + }); + + it('serializes bigints into little-endian byte order', function () { + const testDoc = { a: 0x1234567812345678n }; + const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); + const expectedResult = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 type + '6100', // 'a' key with null terminator + '7856341278563412' + ]) + ); + + expect(expectedResult.value).to.equal(serializedDoc.value); + }); + + it('serializes a BigInt that can be safely represented as a Number', function () { + const testDoc = { a: 0x23n }; + const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); + const expectedResult = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 type + '6100', // 'a' key with null terminator + '2300000000000000' // little endian int64 + ]) + ); + expect(serializedDoc).to.deep.equal(expectedResult); + }); + + it('serializes a BigInt in the valid range [-2^63, 2^63 - 1]', function () { + const testDoc = { a: 0xfffffffffffffff1n }; + const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); + const expectedResult = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 + '6100', // 'a' key with null terminator + 'f1ffffffffffffff' + ]) + ); + expect(serializedDoc).to.deep.equal(expectedResult); + }); + + it('wraps to negative on a BigInt that is larger than (2^63 -1)', function () { + const maxIntPlusOne = { a: 2n ** 63n }; + const serializedMaxIntPlusOne = getSerializedDocParts(BSON.serialize(maxIntPlusOne)); + const expectedResultForMaxIntPlusOne = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 + '6100', // 'a' key with null terminator + '0000000000000080' + ]) + ); + expect(serializedMaxIntPlusOne).to.deep.equal(expectedResultForMaxIntPlusOne); + }); + + it('serializes BigInts at the edges of the valid range [-2^63, 2^63 - 1]', function () { + const maxPositiveInt64 = { a: 2n ** 63n - 1n }; + const serializedMaxPositiveInt64 = getSerializedDocParts(BSON.serialize(maxPositiveInt64)); + const expectedSerializationForMaxPositiveInt64 = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 + '6100', // 'a' key with null terminator + 'ffffffffffffff7f' + ]) + ); + expect(serializedMaxPositiveInt64).to.deep.equal(expectedSerializationForMaxPositiveInt64); + + const minPositiveInt64 = { a: -(2n ** 63n) }; + const serializedMinPositiveInt64 = getSerializedDocParts(BSON.serialize(minPositiveInt64)); + const expectedSerializationForMinPositiveInt64 = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 + '6100', // 'a' key with null terminator + '0000000000000080' + ]) + ); + expect(serializedMinPositiveInt64).to.deep.equal(expectedSerializationForMinPositiveInt64); + }); + + it('truncates a BigInt that is larger than a 64-bit int', function () { + const testDoc = { a: 2n ** 64n + 1n }; + const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); + const expectedSerialization = getSerializedDocParts( + bufferFromHexArray([ + '12', //int64 + '6100', // 'a' key with null terminator + '0100000000000000' + ]) + ); + expect(serializedDoc).to.deep.equal(expectedSerialization); + }); + + it('serializes array of BigInts', function () { + const testArr = { a: [1n] }; + const serializedArr = BSON.serialize(testArr); + const expectedSerialization = bufferFromHexArray([ + '04', // array + '6100', // 'a' key with null terminator + bufferFromHexArray([ + '12', // int64 + '3000', // '0' key with null terminator + '0100000000000000' // 1n (little-endian) + ]).toString('hex') + ]); + expect(serializedArr).to.deep.equal(expectedSerialization); + }); + + it('serializes Map with BigInt values', function () { + const testMap = new Map(); + testMap.set('a', 1n); + const serializedMap = getSerializedDocParts(BSON.serialize(testMap)); + const expectedSerialization = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 + '6100', // 'a' key with null terminator + '0100000000000000' + ]) + ); + expect(serializedMap).to.deep.equal(expectedSerialization); + }); +}); diff --git a/test/node/bigint_tests.js b/test/node/bigint_tests.js deleted file mode 100644 index d20f8def..00000000 --- a/test/node/bigint_tests.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -const BSON = require('../register-bson'); -const BSONTypeError = BSON.BSONTypeError; - -describe('BSON BigInt Support', function () { - before(function () { - try { - BigInt(0); - } catch (_) { - this.skip('JS VM does not support BigInt'); - } - }); - it('Should serialize an int that fits in int32', function () { - const testDoc = { b: BigInt(32) }; - expect(() => BSON.serialize(testDoc)).to.throw(BSONTypeError); - - // const serializedDoc = BSON.serialize(testDoc); - // // prettier-ignore - // const resultBuffer = Buffer.from([0x0C, 0x00, 0x00, 0x00, 0x10, 0x62, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00]); - // const resultDoc = BSON.deserialize(serializedDoc); - // expect(Array.from(serializedDoc)).to.have.members(Array.from(resultBuffer)); - // expect(BigInt(resultDoc.b)).to.equal(testDoc.b); - }); - - it('Should serialize an int that fits in int64', function () { - const testDoc = { b: BigInt(0x1ffffffff) }; - expect(() => BSON.serialize(testDoc)).to.throw(BSONTypeError); - - // const serializedDoc = BSON.serialize(testDoc); - // // prettier-ignore - // const resultBuffer = Buffer.from([0x10, 0x00, 0x00, 0x00, 0x12, 0x62, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00]); - // const resultDoc = BSON.deserialize(serializedDoc); - // expect(Array.from(serializedDoc)).to.have.members(Array.from(resultBuffer)); - // expect(BigInt(resultDoc.b)).to.equal(testDoc.b); - }); - - it('Should serialize an int that fits in decimal128', function () { - const testDoc = { b: BigInt('9223372036854776001') }; // int64 max + 1 - expect(() => BSON.serialize(testDoc)).to.throw(BSONTypeError); - - // const serializedDoc = BSON.serialize(testDoc); - // // prettier-ignore - // const resultBuffer = Buffer.from([0x18, 0x00, 0x00, 0x00, 0x13, 0x62, 0x00, 0xC1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x30, 0x00]); - // const resultDoc = BSON.deserialize(serializedDoc); - // expect(Array.from(serializedDoc)).to.have.members(Array.from(resultBuffer)); - // expect(resultDoc.b._bsontype).to.equal('Decimal128'); - // expect(BigInt(resultDoc.b.toString())).to.equal(testDoc.b); - }); - - it('Should throw if BigInt is too large to serialize', function () { - const testDoc = { - b: BigInt('9'.repeat(35)) - }; // decimal 128 can only encode 34 digits of precision - expect(() => BSON.serialize(testDoc)).to.throw(BSONTypeError); - // expect(() => BSON.serialize(testDoc)).to.throw(); - }); - - it('Should accept BigInts in Long constructor', function (done) { - const Long = BSON.Long; - expect(new Long(BigInt('0')).toString()).to.equal('0'); - expect(new Long(BigInt('-1')).toString()).to.equal('-1'); - expect(new Long(BigInt('-1'), true).toString()).to.equal('18446744073709551615'); - expect(new Long(BigInt('123456789123456789')).toString()).to.equal('123456789123456789'); - expect(new Long(BigInt('123456789123456789'), true).toString()).to.equal('123456789123456789'); - expect(new Long(BigInt('13835058055282163712')).toString()).to.equal('-4611686018427387904'); - expect(new Long(BigInt('13835058055282163712'), true).toString()).to.equal( - '13835058055282163712' - ); - done(); - }); -}); diff --git a/test/node/long.test.ts b/test/node/long.test.ts new file mode 100644 index 00000000..9a73aedc --- /dev/null +++ b/test/node/long.test.ts @@ -0,0 +1,24 @@ +import { Long } from '../register-bson'; + +describe('Long', function () { + it('accepts strings in the constructor', function () { + expect(new Long('0').toString()).to.equal('0'); + expect(new Long('00').toString()).to.equal('0'); + expect(new Long('-1').toString()).to.equal('-1'); + expect(new Long('-1', true).toString()).to.equal('18446744073709551615'); + expect(new Long('123456789123456789').toString()).to.equal('123456789123456789'); + expect(new Long('123456789123456789', true).toString()).to.equal('123456789123456789'); + expect(new Long('13835058055282163712').toString()).to.equal('-4611686018427387904'); + expect(new Long('13835058055282163712', true).toString()).to.equal('13835058055282163712'); + }); + + it('accepts BigInts in Long constructor', function () { + expect(new Long(0n).toString()).to.equal('0'); + expect(new Long(-1n).toString()).to.equal('-1'); + expect(new Long(-1n, true).toString()).to.equal('18446744073709551615'); + expect(new Long(123456789123456789n).toString()).to.equal('123456789123456789'); + expect(new Long(123456789123456789n, true).toString()).to.equal('123456789123456789'); + expect(new Long(13835058055282163712n).toString()).to.equal('-4611686018427387904'); + expect(new Long(13835058055282163712n, true).toString()).to.equal('13835058055282163712'); + }); +}); diff --git a/test/node/long_tests.js b/test/node/long_tests.js deleted file mode 100644 index a1ea2e4c..00000000 --- a/test/node/long_tests.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const BSON = require('../register-bson'); -const Long = BSON.Long; - -describe('Long', function () { - it('accepts strings in the constructor', function (done) { - expect(new Long('0').toString()).to.equal('0'); - expect(new Long('00').toString()).to.equal('0'); - expect(new Long('-1').toString()).to.equal('-1'); - expect(new Long('-1', true).toString()).to.equal('18446744073709551615'); - expect(new Long('123456789123456789').toString()).to.equal('123456789123456789'); - expect(new Long('123456789123456789', true).toString()).to.equal('123456789123456789'); - expect(new Long('13835058055282163712').toString()).to.equal('-4611686018427387904'); - expect(new Long('13835058055282163712', true).toString()).to.equal('13835058055282163712'); - done(); - }); -}); From 9ff60baedf2c3fcd5faf3760d4df03235531c608 Mon Sep 17 00:00:00 2001 From: Warren James <28974128+W-A-James@users.noreply.github.com> Date: Wed, 4 Jan 2023 13:36:18 -0500 Subject: [PATCH 2/4] feat(NODE-4871): Add support for int64 deserialization to BigInt (#542) --- README.md | 3 +- src/parser/deserializer.ts | 33 +++- test/node/bigint.test.ts | 394 +++++++++++++++++++++++-------------- 3 files changed, 273 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index 273fa356..0e7a675e 100644 --- a/README.md +++ b/README.md @@ -238,12 +238,13 @@ Serialize a Javascript object using a predefined Buffer and index into the buffe | buffer | Buffer | | the buffer containing the serialized set of BSON documents. | | [options.evalFunctions] | Object | false | evaluate functions in the BSON document scoped to the object deserialized. | | [options.cacheFunctions] | Object | false | cache evaluated functions for reuse. | +| [options.useBigInt64] | Object | false | when deserializing a Long will return a BigInt. | | [options.promoteLongs] | Object | true | when deserializing a Long will fit it into a Number if it's smaller than 53 bits | | [options.promoteBuffers] | Object | false | when deserializing a Binary will return it as a node.js Buffer instance. | | [options.promoteValues] | Object | false | when deserializing will promote BSON values to their Node.js closest equivalent types. | | [options.fieldsAsRaw] | Object | | allow to specify if there what fields we wish to return as unserialized raw buffer. | | [options.bsonRegExp] | Object | false | return BSON regular expressions as BSONRegExp instances. | -| [options.allowObjectSmallerThanBufferSize] | boolean | false | allows the buffer to be larger than the parsed BSON object | +| [options.allowObjectSmallerThanBufferSize] | boolean | false | allows the buffer to be larger than the parsed BSON object. | Deserialize data as BSON. diff --git a/src/parser/deserializer.ts b/src/parser/deserializer.ts index 1a7d7690..50d7a8d8 100644 --- a/src/parser/deserializer.ts +++ b/src/parser/deserializer.ts @@ -14,12 +14,14 @@ import { ObjectId } from '../objectid'; import { BSONRegExp } from '../regexp'; import { BSONSymbol } from '../symbol'; import { Timestamp } from '../timestamp'; -import { ByteUtils } from '../utils/byte_utils'; +import { BSONDataView, ByteUtils } from '../utils/byte_utils'; import { validateUtf8 } from '../validate_utf8'; /** @public */ export interface DeserializeOptions { - /** when deserializing a Long will fit it into a Number if it's smaller than 53 bits */ + /** when deserializing a Long will return as a BigInt. */ + useBigInt64?: boolean; + /** when deserializing a Long will fit it into a Number if it's smaller than 53 bits. */ promoteLongs?: boolean; /** when deserializing a Binary will return it as a node.js Buffer instance. */ promoteBuffers?: boolean; @@ -29,7 +31,7 @@ export interface DeserializeOptions { fieldsAsRaw?: Document; /** return BSON regular expressions as BSONRegExp instances. */ bsonRegExp?: boolean; - /** allows the buffer to be larger than the parsed BSON object */ + /** allows the buffer to be larger than the parsed BSON object. */ allowObjectSmallerThanBufferSize?: boolean; /** Offset into buffer to begin reading document from */ index?: number; @@ -96,7 +98,7 @@ export function internalDeserialize( ); } - // Start deserializtion + // Start deserialization return deserializeObject(buffer, index, options, isArray); } @@ -117,9 +119,18 @@ function deserializeObject( const bsonRegExp = typeof options['bsonRegExp'] === 'boolean' ? options['bsonRegExp'] : false; // Controls the promotion of values vs wrapper classes - const promoteBuffers = options['promoteBuffers'] == null ? false : options['promoteBuffers']; - const promoteLongs = options['promoteLongs'] == null ? true : options['promoteLongs']; - const promoteValues = options['promoteValues'] == null ? true : options['promoteValues']; + const promoteBuffers = options.promoteBuffers ?? false; + const promoteLongs = options.promoteLongs ?? true; + const promoteValues = options.promoteValues ?? true; + const useBigInt64 = options.useBigInt64 ?? false; + + if (useBigInt64 && !promoteValues) { + throw new BSONError('Must either request bigint or Long for int64 deserialization'); + } + + if (useBigInt64 && !promoteLongs) { + throw new BSONError('Must either request bigint or Long for int64 deserialization'); + } // Ensures default validation option if none given const validation = options.validation == null ? { utf8: true } : options.validation; @@ -323,6 +334,8 @@ function deserializeObject( value = null; } else if (elementType === constants.BSON_DATA_LONG) { // Unpack the low and high bits + const dataview = BSONDataView.fromUint8Array(buffer.subarray(index, index + 8)); + const lowBits = buffer[index++] | (buffer[index++] << 8) | @@ -334,8 +347,10 @@ function deserializeObject( (buffer[index++] << 16) | (buffer[index++] << 24); const long = new Long(lowBits, highBits); - // Promote the long if possible - if (promoteLongs && promoteValues === true) { + if (useBigInt64) { + value = dataview.getBigInt64(0, true); + } else if (promoteLongs && promoteValues === true) { + // Promote the long if possible value = long.lessThanOrEqual(JS_INT_MAX_LONG) && long.greaterThanOrEqual(JS_INT_MIN_LONG) ? long.toNumber() diff --git a/test/node/bigint.test.ts b/test/node/bigint.test.ts index fe771ce3..fcc95790 100644 --- a/test/node/bigint.test.ts +++ b/test/node/bigint.test.ts @@ -1,166 +1,266 @@ -import { BSON } from '../register-bson'; +import { BSON, BSONError } from '../register-bson'; import { bufferFromHexArray } from './tools/utils'; +import { expect } from 'chai'; import { BSON_DATA_LONG } from '../../src/constants'; import { BSONDataView } from '../../src/utils/byte_utils'; -describe('BSON BigInt serialization Support', function () { - // Index for the data type byte of a BSON document with a - // NOTE: These offsets only apply for documents with the shape {a : } - // where n is a BigInt - type SerializedDocParts = { - dataType: number; - key: string; - value: bigint; - }; - /** - * NOTE: this function operates on serialized BSON documents with the shape { : } - * where n is some int64. This function assumes that keys are properly encoded - * with the necessary null byte at the end and only at the end of the key string - */ - function getSerializedDocParts(serializedDoc: Uint8Array): SerializedDocParts { - const DATA_TYPE_OFFSET = 4; - const KEY_OFFSET = 5; - - const dataView = BSONDataView.fromUint8Array(serializedDoc); - const keySlice = serializedDoc.slice(KEY_OFFSET); - - let keyLength = 0; - while (keySlice[keyLength++] !== 0); - - const valueOffset = KEY_OFFSET + keyLength; - const key = Buffer.from(serializedDoc.slice(KEY_OFFSET, KEY_OFFSET + keyLength)).toString( - 'utf8' - ); - - return { - dataType: dataView.getInt8(DATA_TYPE_OFFSET), - key: key.slice(0, keyLength - 1), - value: dataView.getBigInt64(valueOffset, true) +describe('BSON BigInt support', function () { + describe('BSON.deserialize()', function () { + type DeserialzationOptions = { + useBigInt64: boolean | undefined; + promoteValues: boolean | undefined; + promoteLongs: boolean | undefined; }; - } + type TestTableEntry = { + options: DeserialzationOptions; + shouldThrow: boolean; + expectedResult: BSON.Document; + expectedErrorMessage: string; + }; + const testSerializedDoc = bufferFromHexArray(['12', '6100', '2300000000000000']); // key 'a', value 0x23 as int64 + const useBigInt64Values = [true, false, undefined]; + const promoteLongsValues = [true, false, undefined]; + const promoteValuesValues = [true, false, undefined]; - it('serializes bigints with the correct BSON type', function () { - const testDoc = { a: 0n }; - const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); - expect(serializedDoc.dataType).to.equal(BSON_DATA_LONG); - }); + const testTable = useBigInt64Values.flatMap(useBigInt64 => { + return promoteLongsValues.flatMap(promoteLongs => { + return promoteValuesValues.flatMap(promoteValues => { + const useBigInt64Set = useBigInt64 ?? false; + const promoteLongsSet = promoteLongs ?? true; + const promoteValuesSet = promoteValues ?? true; + const shouldThrow = useBigInt64Set && (!promoteValuesSet || !promoteLongsSet); + let expectedResult: BSON.Document; + if (useBigInt64Set) { + expectedResult = { a: 0x23n }; + } else if (promoteLongsSet && promoteValuesSet) { + expectedResult = { a: 0x23 }; + } else { + expectedResult = { a: new BSON.Long(0x23) }; + } + const expectedErrorMessage = shouldThrow + ? 'Must either request bigint or Long for int64 deserialization' + : ''; + return [ + { + options: { useBigInt64, promoteValues, promoteLongs }, + shouldThrow, + expectedResult, + expectedErrorMessage + } + ]; + }); + }); + }); - it('serializes bigints into little-endian byte order', function () { - const testDoc = { a: 0x1234567812345678n }; - const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); - const expectedResult = getSerializedDocParts( - bufferFromHexArray([ - '12', // int64 type - '6100', // 'a' key with null terminator - '7856341278563412' - ]) - ); + it('meta test: generates 27 tests with exactly 5 error cases and 22 success cases', () => { + expect(testTable).to.have.lengthOf(27); + expect(testTable.filter(t => t.shouldThrow)).to.have.lengthOf(5); + expect(testTable.filter(t => !t.shouldThrow)).to.have.lengthOf(22); + }); - expect(expectedResult.value).to.equal(serializedDoc.value); - }); + function generateTestDescription(entry: TestTableEntry): string { + const options = entry.options; + const promoteValues = `promoteValues ${ + options.promoteValues === undefined ? 'is default' : `is ${options.promoteValues}` + }`; + const promoteLongs = `promoteLongs ${ + options.promoteLongs === undefined ? 'is default' : `is ${options.promoteLongs}` + }`; + const useBigInt64 = `useBigInt64 ${ + options.useBigInt64 === undefined ? 'is default' : `is ${options.useBigInt64}` + }`; + const flagString = `${useBigInt64}, ${promoteValues}, and ${promoteLongs}`; + if (entry.shouldThrow) { + return `throws when ${flagString}`; + } else { + return `deserializes int64 to ${entry.expectedResult.a.constructor.name} when ${flagString}`; + } + } - it('serializes a BigInt that can be safely represented as a Number', function () { - const testDoc = { a: 0x23n }; - const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); - const expectedResult = getSerializedDocParts( - bufferFromHexArray([ - '12', // int64 type - '6100', // 'a' key with null terminator - '2300000000000000' // little endian int64 - ]) - ); - expect(serializedDoc).to.deep.equal(expectedResult); - }); + function generateTest(test: TestTableEntry) { + const options = test.options; + const deserialize = () => { + return BSON.deserialize(testSerializedDoc, options); + }; + if (test.shouldThrow) { + return () => { + expect(deserialize).to.throw(BSONError, test.expectedErrorMessage); + }; + } else { + return () => { + const deserializedDoc = deserialize(); + expect(deserializedDoc).to.deep.equal(test.expectedResult); + }; + } + } - it('serializes a BigInt in the valid range [-2^63, 2^63 - 1]', function () { - const testDoc = { a: 0xfffffffffffffff1n }; - const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); - const expectedResult = getSerializedDocParts( - bufferFromHexArray([ - '12', // int64 - '6100', // 'a' key with null terminator - 'f1ffffffffffffff' - ]) - ); - expect(serializedDoc).to.deep.equal(expectedResult); - }); + for (const tableEntry of testTable) { + const test = generateTest(tableEntry); + const description = generateTestDescription(tableEntry); - it('wraps to negative on a BigInt that is larger than (2^63 -1)', function () { - const maxIntPlusOne = { a: 2n ** 63n }; - const serializedMaxIntPlusOne = getSerializedDocParts(BSON.serialize(maxIntPlusOne)); - const expectedResultForMaxIntPlusOne = getSerializedDocParts( - bufferFromHexArray([ - '12', // int64 - '6100', // 'a' key with null terminator - '0000000000000080' - ]) - ); - expect(serializedMaxIntPlusOne).to.deep.equal(expectedResultForMaxIntPlusOne); + it(description, test); + } }); - it('serializes BigInts at the edges of the valid range [-2^63, 2^63 - 1]', function () { - const maxPositiveInt64 = { a: 2n ** 63n - 1n }; - const serializedMaxPositiveInt64 = getSerializedDocParts(BSON.serialize(maxPositiveInt64)); - const expectedSerializationForMaxPositiveInt64 = getSerializedDocParts( - bufferFromHexArray([ - '12', // int64 - '6100', // 'a' key with null terminator - 'ffffffffffffff7f' - ]) - ); - expect(serializedMaxPositiveInt64).to.deep.equal(expectedSerializationForMaxPositiveInt64); - - const minPositiveInt64 = { a: -(2n ** 63n) }; - const serializedMinPositiveInt64 = getSerializedDocParts(BSON.serialize(minPositiveInt64)); - const expectedSerializationForMinPositiveInt64 = getSerializedDocParts( - bufferFromHexArray([ - '12', // int64 - '6100', // 'a' key with null terminator - '0000000000000080' - ]) - ); - expect(serializedMinPositiveInt64).to.deep.equal(expectedSerializationForMinPositiveInt64); - }); + describe('BSON.serialize()', function () { + // Index for the data type byte of a BSON document with a + // NOTE: These offsets only apply for documents with the shape {a : } + // where n is a BigInt + type SerializedDocParts = { + dataType: number; + key: string; + value: bigint; + }; + /** + * NOTE: this function operates on serialized BSON documents with the shape { : } + * where n is some int64. This function assumes that keys are properly encoded + * with the necessary null byte at the end and only at the end of the key string + */ + function getSerializedDocParts(serializedDoc: Uint8Array): SerializedDocParts { + const DATA_TYPE_OFFSET = 4; + const KEY_OFFSET = 5; - it('truncates a BigInt that is larger than a 64-bit int', function () { - const testDoc = { a: 2n ** 64n + 1n }; - const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); - const expectedSerialization = getSerializedDocParts( - bufferFromHexArray([ - '12', //int64 - '6100', // 'a' key with null terminator - '0100000000000000' - ]) - ); - expect(serializedDoc).to.deep.equal(expectedSerialization); - }); + const dataView = BSONDataView.fromUint8Array(serializedDoc); + const keySlice = serializedDoc.slice(KEY_OFFSET); - it('serializes array of BigInts', function () { - const testArr = { a: [1n] }; - const serializedArr = BSON.serialize(testArr); - const expectedSerialization = bufferFromHexArray([ - '04', // array - '6100', // 'a' key with null terminator - bufferFromHexArray([ - '12', // int64 - '3000', // '0' key with null terminator - '0100000000000000' // 1n (little-endian) - ]).toString('hex') - ]); - expect(serializedArr).to.deep.equal(expectedSerialization); - }); + let keyLength = 0; + while (keySlice[keyLength++] !== 0); + + const valueOffset = KEY_OFFSET + keyLength; + const key = Buffer.from(serializedDoc.slice(KEY_OFFSET, KEY_OFFSET + keyLength)).toString( + 'utf8' + ); + + return { + dataType: dataView.getInt8(DATA_TYPE_OFFSET), + key: key.slice(0, keyLength - 1), + value: dataView.getBigInt64(valueOffset, true) + }; + } + + it('serializes bigints with the correct BSON type', function () { + const testDoc = { a: 0n }; + const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); + expect(serializedDoc.dataType).to.equal(BSON_DATA_LONG); + }); + + it('serializes bigints into little-endian byte order', function () { + const testDoc = { a: 0x1234567812345678n }; + const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); + const expectedResult = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 type + '6100', // 'a' key with null terminator + '7856341278563412' + ]) + ); + + expect(expectedResult.value).to.equal(serializedDoc.value); + }); - it('serializes Map with BigInt values', function () { - const testMap = new Map(); - testMap.set('a', 1n); - const serializedMap = getSerializedDocParts(BSON.serialize(testMap)); - const expectedSerialization = getSerializedDocParts( - bufferFromHexArray([ - '12', // int64 + it('serializes a BigInt that can be safely represented as a Number', function () { + const testDoc = { a: 0x23n }; + const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); + const expectedResult = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 type + '6100', // 'a' key with null terminator + '2300000000000000' // little endian int64 + ]) + ); + expect(serializedDoc).to.deep.equal(expectedResult); + }); + + it('serializes a BigInt in the valid range [-2^63, 2^63 - 1]', function () { + const testDoc = { a: 0xfffffffffffffff1n }; + const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); + const expectedResult = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 + '6100', // 'a' key with null terminator + 'f1ffffffffffffff' + ]) + ); + expect(serializedDoc).to.deep.equal(expectedResult); + }); + + it('wraps to negative on a BigInt that is larger than (2^63 -1)', function () { + const maxIntPlusOne = { a: 2n ** 63n }; + const serializedMaxIntPlusOne = getSerializedDocParts(BSON.serialize(maxIntPlusOne)); + const expectedResultForMaxIntPlusOne = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 + '6100', // 'a' key with null terminator + '0000000000000080' + ]) + ); + expect(serializedMaxIntPlusOne).to.deep.equal(expectedResultForMaxIntPlusOne); + }); + + it('serializes BigInts at the edges of the valid range [-2^63, 2^63 - 1]', function () { + const maxPositiveInt64 = { a: 2n ** 63n - 1n }; + const serializedMaxPositiveInt64 = getSerializedDocParts(BSON.serialize(maxPositiveInt64)); + const expectedSerializationForMaxPositiveInt64 = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 + '6100', // 'a' key with null terminator + 'ffffffffffffff7f' + ]) + ); + expect(serializedMaxPositiveInt64).to.deep.equal(expectedSerializationForMaxPositiveInt64); + + const minPositiveInt64 = { a: -(2n ** 63n) }; + const serializedMinPositiveInt64 = getSerializedDocParts(BSON.serialize(minPositiveInt64)); + const expectedSerializationForMinPositiveInt64 = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 + '6100', // 'a' key with null terminator + '0000000000000080' + ]) + ); + expect(serializedMinPositiveInt64).to.deep.equal(expectedSerializationForMinPositiveInt64); + }); + + it('truncates a BigInt that is larger than a 64-bit int', function () { + const testDoc = { a: 2n ** 64n + 1n }; + const serializedDoc = getSerializedDocParts(BSON.serialize(testDoc)); + const expectedSerialization = getSerializedDocParts( + bufferFromHexArray([ + '12', //int64 + '6100', // 'a' key with null terminator + '0100000000000000' + ]) + ); + expect(serializedDoc).to.deep.equal(expectedSerialization); + }); + + it('serializes array of BigInts', function () { + const testArr = { a: [1n] }; + const serializedArr = BSON.serialize(testArr); + const expectedSerialization = bufferFromHexArray([ + '04', // array '6100', // 'a' key with null terminator - '0100000000000000' - ]) - ); - expect(serializedMap).to.deep.equal(expectedSerialization); + bufferFromHexArray([ + '12', // int64 + '3000', // '0' key with null terminator + '0100000000000000' // 1n (little-endian) + ]).toString('hex') + ]); + expect(serializedArr).to.deep.equal(expectedSerialization); + }); + + it('serializes Map with BigInt values', function () { + const testMap = new Map(); + testMap.set('a', 1n); + const serializedMap = getSerializedDocParts(BSON.serialize(testMap)); + const expectedSerialization = getSerializedDocParts( + bufferFromHexArray([ + '12', // int64 + '6100', // 'a' key with null terminator + '0100000000000000' + ]) + ); + expect(serializedMap).to.deep.equal(expectedSerialization); + }); }); }); From e0dbb17d0a13f3aca9879f08cba3573b2a84fd8d Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 4 Jan 2023 15:21:24 -0500 Subject: [PATCH 3/4] fix(NODE-4905): double precision accuracy in canonical EJSON (#548) Co-authored-by: Durran Jordan --- src/double.ts | 16 ++++-------- test/node/bson_corpus.spec.test.js | 40 +++++++++++++++++++++++++++--- test/node/double.test.ts | 25 +++++++++++++++++++ 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/src/double.ts b/src/double.ts index 7a4feb64..5e790017 100644 --- a/src/double.ts +++ b/src/double.ts @@ -52,23 +52,17 @@ export class Double { return this.value; } - // NOTE: JavaScript has +0 and -0, apparently to model limit calculations. If a user - // explicitly provided `-0` then we need to ensure the sign makes it into the output if (Object.is(Math.sign(this.value), -0)) { - return { $numberDouble: `-${this.value.toFixed(1)}` }; + // NOTE: JavaScript has +0 and -0, apparently to model limit calculations. If a user + // explicitly provided `-0` then we need to ensure the sign makes it into the output + return { $numberDouble: '-0.0' }; } - let $numberDouble: string; if (Number.isInteger(this.value)) { - $numberDouble = this.value.toFixed(1); - if ($numberDouble.length >= 13) { - $numberDouble = this.value.toExponential(13).toUpperCase(); - } + return { $numberDouble: `${this.value}.0` }; } else { - $numberDouble = this.value.toString(); + return { $numberDouble: `${this.value}` }; } - - return { $numberDouble }; } /** @internal */ diff --git a/test/node/bson_corpus.spec.test.js b/test/node/bson_corpus.spec.test.js index 7b98bb50..36828bef 100644 --- a/test/node/bson_corpus.spec.test.js +++ b/test/node/bson_corpus.spec.test.js @@ -184,8 +184,26 @@ describe('BSON Corpus', function () { // convert inputs to native Javascript objects const nativeFromCB = bsonToNative(cB); - // round tripped EJSON should match the original - expect(nativeToCEJSON(jsonToNative(cEJ))).to.equal(cEJ); + if (cEJ.includes('1.2345678921232E+18')) { + // The following is special test logic for a "Double type" bson corpus test that uses a different + // string format for the resulting double value + // The test does not have a loss in precision, just different exponential output + // We want to ensure that the stringified value when interpreted as a double is equal + // as opposed to the string being precisely the same + if (description !== 'Double type') { + throw new Error('Unexpected test using 1.2345678921232E+18'); + } + const eJSONParsedAsJSON = JSON.parse(cEJ); + const eJSONParsed = EJSON.parse(cEJ, { relaxed: false }); + expect(eJSONParsedAsJSON).to.have.nested.property('d.$numberDouble'); + expect(eJSONParsed).to.have.nested.property('d._bsontype', 'Double'); + const testInputAsFloat = Number.parseFloat(eJSONParsedAsJSON.d.$numberDouble); + const ejsonOutputAsFloat = eJSONParsed.d.valueOf(); + expect(ejsonOutputAsFloat).to.equal(testInputAsFloat); + } else { + // round tripped EJSON should match the original + expect(nativeToCEJSON(jsonToNative(cEJ))).to.equal(cEJ); + } // invalid, but still parseable, EJSON. if provided, make sure that we // properly convert it to canonical EJSON and BSON. @@ -205,8 +223,22 @@ describe('BSON Corpus', function () { expect(nativeToBson(jsonToNative(cEJ))).to.deep.equal(cB); } - // the reverse direction, BSON -> native -> EJSON, should match canonical EJSON. - expect(nativeToCEJSON(nativeFromCB)).to.equal(cEJ); + if (cEJ.includes('1.2345678921232E+18')) { + // The round tripped value should be equal in interpreted value, not in exact character match + const eJSONFromBSONAsJSON = JSON.parse( + EJSON.stringify(BSON.deserialize(cB), { relaxed: false }) + ); + const eJSONParsed = EJSON.parse(cEJ, { relaxed: false }); + // TODO(NODE-4377): EJSON transforms large doubles into longs + expect(eJSONFromBSONAsJSON).to.have.nested.property('d.$numberLong'); + expect(eJSONParsed).to.have.nested.property('d._bsontype', 'Double'); + const testInputAsFloat = Number.parseFloat(eJSONFromBSONAsJSON.d.$numberLong); + const ejsonOutputAsFloat = eJSONParsed.d.valueOf(); + expect(ejsonOutputAsFloat).to.equal(testInputAsFloat); + } else { + // the reverse direction, BSON -> native -> EJSON, should match canonical EJSON. + expect(nativeToCEJSON(nativeFromCB)).to.equal(cEJ); + } if (v.relaxed_extjson) { let rEJ = normalize(v.relaxed_extjson); diff --git a/test/node/double.test.ts b/test/node/double.test.ts index 00ed30f8..450017a4 100644 --- a/test/node/double.test.ts +++ b/test/node/double.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { BSON, Double } from '../register-bson'; import { BSON_DATA_NUMBER, BSON_DATA_INT } from '../../src/constants'; +import { inspect } from 'node:util'; describe('BSON Double Precision', function () { context('class Double', function () { @@ -36,6 +37,30 @@ describe('BSON Double Precision', function () { }); } }); + + describe('.toExtendedJSON()', () => { + const tests = [ + { input: new Double(0), output: { $numberDouble: '0.0' } }, + { input: new Double(-0), output: { $numberDouble: '-0.0' } }, + { input: new Double(3), output: { $numberDouble: '3.0' } }, + { input: new Double(-3), output: { $numberDouble: '-3.0' } }, + { input: new Double(3.4), output: { $numberDouble: '3.4' } }, + { input: new Double(Number.EPSILON), output: { $numberDouble: '2.220446049250313e-16' } }, + { input: new Double(12345e7), output: { $numberDouble: '123450000000.0' } }, + { input: new Double(12345e-1), output: { $numberDouble: '1234.5' } }, + { input: new Double(-12345e-1), output: { $numberDouble: '-1234.5' } }, + { input: new Double(Infinity), output: { $numberDouble: 'Infinity' } }, + { input: new Double(-Infinity), output: { $numberDouble: '-Infinity' } }, + { input: new Double(NaN), output: { $numberDouble: 'NaN' } } + ]; + + for (const { input, output } of tests) { + const title = `returns ${inspect(output)} when Double is ${input}`; + it(title, () => { + expect(output).to.deep.equal(input.toExtendedJSON({ relaxed: false })); + }); + } + }); }); function serializeThenDeserialize(value) { From 9679ec3d98a986dcdf68afe625659d407044c547 Mon Sep 17 00:00:00 2001 From: Neal Beeken Date: Wed, 4 Jan 2023 16:52:08 -0500 Subject: [PATCH 4/4] test(NODE-4910): clean up removed BSONTypeError from tests (#546) --- test/node/bson_test.js | 10 +++++----- test/node/extended_json.test.ts | 5 +++-- test/node/object_id_tests.js | 33 +++++++++++++++------------------ test/node/uuid_tests.js | 11 +++++------ test/register-bson.js | 9 +++++++++ 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/test/node/bson_test.js b/test/node/bson_test.js index b71c4d6e..ee2c5b9a 100644 --- a/test/node/bson_test.js +++ b/test/node/bson_test.js @@ -1410,14 +1410,14 @@ describe('BSON', function () { expect(() => { parser.deserialize(data); - }).to.throw(); + }).to.throw(BSONError); data = Buffer.alloc(5); data[0] = 0xff; data[1] = 0xff; expect(() => { parser.deserialize(data); - }).to.throw(); + }).to.throw(BSONError); // Finish up done(); @@ -1896,9 +1896,9 @@ describe('BSON', function () { ['c', badArray] ]); - expect(() => BSON.serialize(badDoc)).to.throw(); - expect(() => BSON.serialize(badArray)).to.throw(); - expect(() => BSON.serialize(badMap)).to.throw(); + expect(() => BSON.serialize(badDoc)).to.throw(BSONError); + expect(() => BSON.serialize(badArray)).to.throw(BSONError); + expect(() => BSON.serialize(badMap)).to.throw(BSONError); }); describe('Should support util.inspect for', function () { diff --git a/test/node/extended_json.test.ts b/test/node/extended_json.test.ts index 4a372088..3e25c559 100644 --- a/test/node/extended_json.test.ts +++ b/test/node/extended_json.test.ts @@ -2,6 +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'; // BSON types const Binary = BSON.Binary; @@ -511,8 +512,8 @@ describe('Extended JSON', function () { const badDoc = { bad: badBsonType }; const badArray = [oid, badDoc]; // const badMap = new Map([['a', badBsonType], ['b', badDoc], ['c', badArray]]); - expect(() => EJSON.serialize(badDoc)).to.throw(); - expect(() => EJSON.serialize(badArray)).to.throw(); + expect(() => EJSON.serialize(badDoc)).to.throw(BSONError); + expect(() => EJSON.serialize(badArray)).to.throw(BSONError); // expect(() => EJSON.serialize(badMap)).to.throw(); // uncomment when EJSON supports ES6 Map }); diff --git a/test/node/object_id_tests.js b/test/node/object_id_tests.js index 0d3e37b3..8bbf4420 100644 --- a/test/node/object_id_tests.js +++ b/test/node/object_id_tests.js @@ -1,10 +1,7 @@ 'use strict'; const Buffer = require('buffer').Buffer; -const BSON = require('../register-bson'); -const EJSON = BSON.EJSON; -const BSONTypeError = BSON.BSONTypeError; -const ObjectId = BSON.ObjectId; +const { BSON, BSONError, EJSON, ObjectId } = require('../register-bson'); const util = require('util'); const { expect } = require('chai'); const { bufferFromHexArray } = require('./tools/utils'); @@ -103,7 +100,7 @@ describe('ObjectId', function () { for (const { input, description } of invalidInputs) { it(`should throw error if ${description} is passed in`, function () { - expect(() => new ObjectId(input)).to.throw(BSONTypeError); + expect(() => new ObjectId(input)).to.throw(BSONError); }); } @@ -114,7 +111,7 @@ describe('ObjectId', function () { return noArgObjID.toHexString(); } }; - expect(() => new ObjectId(objectIdLike)).to.throw(BSONTypeError); + expect(() => new ObjectId(objectIdLike)).to.throw(BSONError); }); it('should correctly create ObjectId from object with valid string id', function () { @@ -186,15 +183,15 @@ describe('ObjectId', function () { const objectNullId = { id: null }; - expect(() => new ObjectId(objectNumId)).to.throw(BSONTypeError); - expect(() => new ObjectId(objectNullId)).to.throw(BSONTypeError); + expect(() => new ObjectId(objectNumId)).to.throw(BSONError); + expect(() => new ObjectId(objectNullId)).to.throw(BSONError); }); it('should throw an error if object with invalid string id is passed in', function () { const objectInvalid24HexStr = { id: 'FFFFFFFFFFFFFFFFFFFFFFFG' }; - expect(() => new ObjectId(objectInvalid24HexStr)).to.throw(BSONTypeError); + expect(() => new ObjectId(objectInvalid24HexStr)).to.throw(BSONError); }); it('should correctly create ObjectId from object with invalid string id and toHexString method', function () { @@ -213,7 +210,7 @@ describe('ObjectId', function () { const objectInvalidBuffer = { id: Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) }; - expect(() => new ObjectId(objectInvalidBuffer)).to.throw(BSONTypeError); + expect(() => new ObjectId(objectInvalidBuffer)).to.throw(BSONError); }); it('should correctly create ObjectId from object with invalid Buffer id and toHexString method', function () { @@ -270,11 +267,11 @@ describe('ObjectId', function () { }); it('should throw error if non-12 byte non-24 hex string passed in', function () { - expect(() => new ObjectId('FFFFFFFFFFFFFFFFFFFFFFFG')).to.throw(BSONTypeError); - expect(() => new ObjectId('thisstringisdefinitelytoolong')).to.throw(BSONTypeError); - expect(() => new ObjectId('tooshort')).to.throw(BSONTypeError); - expect(() => new ObjectId('101010')).to.throw(BSONTypeError); - expect(() => new ObjectId('')).to.throw(BSONTypeError); + expect(() => new ObjectId('FFFFFFFFFFFFFFFFFFFFFFFG')).to.throw(BSONError); + expect(() => new ObjectId('thisstringisdefinitelytoolong')).to.throw(BSONError); + expect(() => new ObjectId('tooshort')).to.throw(BSONError); + expect(() => new ObjectId('101010')).to.throw(BSONError); + expect(() => new ObjectId('')).to.throw(BSONError); }); it('should correctly create ObjectId from 24 hex string', function () { @@ -319,7 +316,7 @@ describe('ObjectId', function () { it('should throw an error if invalid Buffer passed in', function () { const a = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); - expect(() => new ObjectId(a)).to.throw(BSONTypeError); + expect(() => new ObjectId(a)).to.throw(BSONError); }); it('should correctly allow for node.js inspect to work with ObjectId', function (done) { @@ -345,11 +342,11 @@ describe('ObjectId', function () { const characterCodesLargerThan256 = 'abcdefŽhijkl'; const length12Not12Bytes = '🐶🐶🐶🐶🐶🐶'; expect(() => new ObjectId(characterCodesLargerThan256).toHexString()).to.throw( - BSONTypeError, + BSONError, 'Argument passed in must be a string of 12 bytes' ); expect(() => new ObjectId(length12Not12Bytes).id).to.throw( - BSONTypeError, + BSONError, 'Argument passed in must be a string of 12 bytes' ); }); diff --git a/test/node/uuid_tests.js b/test/node/uuid_tests.js index 9224f7b6..f5088c4a 100644 --- a/test/node/uuid_tests.js +++ b/test/node/uuid_tests.js @@ -4,8 +4,7 @@ const { Buffer } = require('buffer'); const { Binary, UUID } = require('../register-bson'); const { inspect } = require('util'); const { validate: uuidStringValidate, version: uuidStringVersion } = require('uuid'); -const BSON = require('../register-bson'); -const BSONTypeError = BSON.BSONTypeError; +const { BSON, BSONError } = require('../register-bson'); const BSON_DATA_BINARY = BSON.BSONType.binData; const { BSON_BINARY_SUBTYPE_UUID_NEW } = require('../../src/constants'); @@ -80,7 +79,7 @@ describe('UUID', () => { */ it('should throw if passed invalid 36-char uuid hex string', () => { expect(() => new UUID(LOWERCASE_DASH_SEPARATED_UUID_STRING)).to.not.throw(); - expect(() => new UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')).to.throw(BSONTypeError); + expect(() => new UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')).to.throw(BSONError); // Note: The version is missing here ^ }); @@ -89,7 +88,7 @@ describe('UUID', () => { */ it('should throw if passed unsupported argument', () => { expect(() => new UUID(LOWERCASE_DASH_SEPARATED_UUID_STRING)).to.not.throw(); - expect(() => new UUID({})).to.throw(BSONTypeError); + expect(() => new UUID({})).to.throw(BSONError); }); /** @@ -142,13 +141,13 @@ describe('UUID', () => { const validRandomBuffer = Buffer.from('Hello World!'); const binRand = new Binary(validRandomBuffer); - expect(() => binRand.toUUID()).to.throw(); + expect(() => binRand.toUUID()).to.throw(BSONError); const validUuidV3String = '25f0d698-15b9-3a7a-96b1-a573061e29c9'; const validUuidV3Buffer = Buffer.from(validUuidV3String.replace(/-/g, ''), 'hex'); const binV3 = new Binary(validUuidV3Buffer, Binary.SUBTYPE_UUID_OLD); - expect(() => binV3.toUUID()).to.throw(); + expect(() => binV3.toUUID()).to.throw(BSONError); const validUuidV4String = 'bd2d74fe-bad8-430c-aeac-b01d073a1eb6'; const validUuidV4Buffer = Buffer.from(validUuidV4String.replace(/-/g, ''), 'hex'); diff --git a/test/register-bson.js b/test/register-bson.js index 3dd57b58..37fda7e0 100644 --- a/test/register-bson.js +++ b/test/register-bson.js @@ -19,6 +19,15 @@ const { loadCJSModuleBSON } = require('./load_bson'); chai.use(function (chai) { const throwsAssertion = chai.Assertion.prototype.throw; chai.Assertion.addMethod('throw', function (...args) { + const isNegated = chai.util.flag(this, 'negate'); + if (!isNegated) { + // to.not.throw() is acceptable to not have an argument + // to.throw() should always provide an Error class + expect(args).to.have.length.of.at.least(1); + // prevent to.throw(undefined) nor to.throw(null) + expect(args[0]).to.exist; + } + try { throwsAssertion.call(this, ...args); } catch (assertionError) {