diff --git a/README.md b/README.md index 0240bed9..273fa356 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,29 @@ Deserialize stream data as BSON documents. **Returns**: Number - returns the next index in the buffer after deserialization **x** numbers of documents. +## Error Handling + +It is our recommendation to use `BSONError.isBSONError()` checks on errors and to avoid relying on parsing `error.message` and `error.name` strings in your code. We guarantee `BSONError.isBSONError()` checks will pass according to semver guidelines, but errors may be sub-classed or their messages may change at any time, even patch releases, as we see fit to increase the helpfulness of the errors. + +Any new errors we add to the driver will directly extend an existing error class and no existing error will be moved to a different parent class outside of a major release. +This means `BSONError.isBSONError()` will always be able to accurately capture the errors that our BSON library throws. + +Hypothetical example: A collection in our Db has an issue with UTF-8 data: + +```ts +let documentCount = 0; +const cursor = collection.find({}, { utf8Validation: true }); +try { + for await (const doc of cursor) documentCount += 1; +} catch (error) { + if (BSONError.isBSONError(error)) { + console.log(`Found the troublemaker UTF-8!: ${documentCount} ${error.message}`); + return documentCount; + } + throw error; +} +``` + ## FAQ #### Why does `undefined` get converted to `null`? diff --git a/docs/upgrade-to-v5.md b/docs/upgrade-to-v5.md index a1a8cc23..01be3b26 100644 --- a/docs/upgrade-to-v5.md +++ b/docs/upgrade-to-v5.md @@ -264,3 +264,27 @@ You can now find compiled bundles of the BSON library in 3 common formats in the - ES Module - `lib/bson.mjs` - Immediate Invoked Function Expression (IIFE) - `lib/bson.bundle.js` - Typically used when trying to import JS on the web CDN style, but the ES Module (`.mjs`) bundle is fully browser compatible and should be preferred if it works in your use case. + +### `BSONTypeError` removed and `BSONError` offers filtering functionality with `static isBSONError()` + +`BSONTypeError` has been removed because it was not a subclass of BSONError so would not return true for an `instanceof` check against `BSONError`. To learn more about our expectations of error handling see [this section of the mongodb driver's readme](https://github.com/mongodb/node-mongodb-native/tree/main#error-handling). + + +A `BSONError` can be thrown from deep within a library that relies on BSON, having one error super class for the library helps with programmatic filtering of an error's origin. +Since BSON can be used in environments where instances may originate from across realms, `BSONError` has a static `isBSONError()` method that helps with determining if an object is a `BSONError` instance (much like [Array.isArray](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray)). +It is our recommendation to use `isBSONError()` checks on errors and to avoid relying on parsing `error.message` and `error.name` strings in your code. We guarantee `isBSONError()` checks will pass according to semver guidelines, but errors may be sub-classed or their messages may change at any time, even patch releases, as we see fit to increase the helpfulness of the errors. + +Hypothetical example: A collection in our Db has an issue with UTF-8 data: +```ts +let documentCount = 0; +const cursor = collection.find({}, { utf8Validation: true }); +try { + for await (const doc of cursor) documentCount += 1; +} catch (error) { + if (BSONError.isBSONError(error)) { + console.log(`Found the troublemaker UTF-8!: ${documentCount} ${error.message}`); + return documentCount; + } + throw error; +} +``` diff --git a/src/binary.ts b/src/binary.ts index 72f2eeb5..0b0113f8 100644 --- a/src/binary.ts +++ b/src/binary.ts @@ -1,7 +1,7 @@ import { bufferToUuidHexString, uuidHexStringToBuffer, uuidValidateString } from './uuid_utils'; import { isUint8Array } from './parser/utils'; import type { EJSONOptions } from './extended_json'; -import { BSONError, BSONTypeError } from './error'; +import { BSONError } from './error'; import { BSON_BINARY_SUBTYPE_UUID_NEW, BSON_MAJOR_VERSION } from './constants'; import { ByteUtils } from './utils/byte_utils'; @@ -86,7 +86,7 @@ export class Binary { !(buffer instanceof ArrayBuffer) && !Array.isArray(buffer) ) { - throw new BSONTypeError( + throw new BSONError( 'Binary can only be constructed from string, Buffer, TypedArray, or Array' ); } @@ -121,9 +121,9 @@ export class Binary { put(byteValue: string | number | Uint8Array | number[]): void { // If it's a string and a has more than one character throw an error if (typeof byteValue === 'string' && byteValue.length !== 1) { - throw new BSONTypeError('only accepts single character String'); + throw new BSONError('only accepts single character String'); } else if (typeof byteValue !== 'number' && byteValue.length !== 1) - throw new BSONTypeError('only accepts single character Uint8Array or Array'); + throw new BSONError('only accepts single character Uint8Array or Array'); // Decode the byte value once let decodedByte: number; @@ -136,7 +136,7 @@ export class Binary { } if (decodedByte < 0 || decodedByte > 255) { - throw new BSONTypeError('only accepts number in a valid unsigned byte range 0-255'); + throw new BSONError('only accepts number in a valid unsigned byte range 0-255'); } if (this.buffer.byteLength > this.position) { @@ -283,7 +283,7 @@ export class Binary { data = uuidHexStringToBuffer(doc.$uuid); } if (!data) { - throw new BSONTypeError(`Unexpected Binary Extended JSON format ${JSON.stringify(doc)}`); + throw new BSONError(`Unexpected Binary Extended JSON format ${JSON.stringify(doc)}`); } return type === BSON_BINARY_SUBTYPE_UUID_NEW ? new UUID(data) : new Binary(data, type); } @@ -337,7 +337,7 @@ export class UUID extends Binary { } else if (typeof input === 'string') { bytes = uuidHexStringToBuffer(input); } else { - throw new BSONTypeError( + throw new BSONError( 'Argument passed in UUID constructor must be a UUID, a 16 byte Buffer or a 32/36 character hex string (dashes excluded/included, format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).' ); } diff --git a/src/bson.ts b/src/bson.ts index 871dac3b..b0684731 100644 --- a/src/bson.ts +++ b/src/bson.ts @@ -49,7 +49,7 @@ export { BSONRegExp, Decimal128 }; -export { BSONError, BSONTypeError } from './error'; +export { BSONError } from './error'; export { BSONType } from './constants'; export { EJSON } from './extended_json'; diff --git a/src/decimal128.ts b/src/decimal128.ts index 81384a24..e2f097b6 100644 --- a/src/decimal128.ts +++ b/src/decimal128.ts @@ -1,5 +1,5 @@ import { BSON_MAJOR_VERSION } from './constants'; -import { BSONTypeError } from './error'; +import { BSONError } from './error'; import { Long } from './long'; import { isUint8Array } from './parser/utils'; import { ByteUtils } from './utils/byte_utils'; @@ -114,7 +114,7 @@ function lessThan(left: Long, right: Long): boolean { } function invalidErr(string: string, message: string) { - throw new BSONTypeError(`"${string}" is not a valid Decimal128 string - ${message}`); + throw new BSONError(`"${string}" is not a valid Decimal128 string - ${message}`); } /** @public */ @@ -147,11 +147,11 @@ export class Decimal128 { this.bytes = Decimal128.fromString(bytes).bytes; } else if (isUint8Array(bytes)) { if (bytes.byteLength !== 16) { - throw new BSONTypeError('Decimal128 must take a Buffer of 16 bytes'); + throw new BSONError('Decimal128 must take a Buffer of 16 bytes'); } this.bytes = bytes; } else { - throw new BSONTypeError('Decimal128 must take a Buffer or string'); + throw new BSONError('Decimal128 must take a Buffer or string'); } } @@ -206,7 +206,7 @@ export class Decimal128 { // TODO: implementing a custom parsing for this, or refactoring the regex would yield // further gains. if (representation.length >= 7000) { - throw new BSONTypeError('' + representation + ' not a valid Decimal128 string'); + throw new BSONError('' + representation + ' not a valid Decimal128 string'); } // Results @@ -216,7 +216,7 @@ export class Decimal128 { // Validate the string if ((!stringMatch && !infMatch && !nanMatch) || representation.length === 0) { - throw new BSONTypeError('' + representation + ' not a valid Decimal128 string'); + throw new BSONError('' + representation + ' not a valid Decimal128 string'); } if (stringMatch) { @@ -288,7 +288,7 @@ export class Decimal128 { } if (sawRadix && !nDigitsRead) - throw new BSONTypeError('' + representation + ' not a valid Decimal128 string'); + throw new BSONError('' + representation + ' not a valid Decimal128 string'); // Read exponent if exists if (representation[index] === 'e' || representation[index] === 'E') { diff --git a/src/error.ts b/src/error.ts index fa5dbb06..88e16b3e 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,21 +1,45 @@ -/** @public */ +/** + * @public + * `BSONError` objects are thrown when runtime errors occur. + */ export class BSONError extends Error { - constructor(message: string) { - super(message); + /** + * @internal + * The underlying algorithm for isBSONError may change to improve how strict it is + * about determining if an input is a BSONError. But it must remain backwards compatible + * with previous minors & patches of the current major version. + */ + protected get bsonError(): true { + return true; } - get name(): string { + override get name(): string { return 'BSONError'; } -} -/** @public */ -export class BSONTypeError extends TypeError { constructor(message: string) { super(message); } - get name(): string { - return 'BSONTypeError'; + /** + * @public + * + * All errors thrown from the BSON library inherit from `BSONError`. + * This method can assist with determining if an error originates from the BSON library + * even if it does not pass an `instanceof` check against this class' constructor. + * + * @param value - any javascript value that needs type checking + */ + public static isBSONError(value: unknown): value is BSONError { + return ( + value != null && + typeof value === 'object' && + 'bsonError' in value && + value.bsonError === true && + // Do not access the following properties, just check existence + 'name' in value && + 'message' in value && + 'stack' in value + ); } } diff --git a/src/extended_json.ts b/src/extended_json.ts index dcfef767..32ee886a 100644 --- a/src/extended_json.ts +++ b/src/extended_json.ts @@ -5,7 +5,7 @@ import { BSON_INT32_MAX, BSON_INT32_MIN, BSON_INT64_MAX, BSON_INT64_MIN } from ' import { DBRef, isDBRefLike } from './db_ref'; import { Decimal128 } from './decimal128'; import { Double } from './double'; -import { BSONError, BSONTypeError } from './error'; +import { BSONError } from './error'; import { Int32 } from './int_32'; import { Long } from './long'; import { MaxKey } from './max_key'; @@ -192,7 +192,7 @@ function serializeValue(value: any, options: EJSONSerializeOptions): any { circularPart.length + (alreadySeen.length + current.length) / 2 - 1 ); - throw new BSONTypeError( + throw new BSONError( 'Converting circular structure to EJSON:\n' + ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + ` ${leadingSpace}\\${dashes}/` @@ -324,7 +324,7 @@ function serializeDocument(doc: any, options: EJSONSerializeOptions) { // Copy the object into this library's version of that type. const mapper = BSON_TYPE_MAPPINGS[doc._bsontype]; if (!mapper) { - throw new BSONTypeError('Unrecognized or invalid _bsontype: ' + doc._bsontype); + throw new BSONError('Unrecognized or invalid _bsontype: ' + doc._bsontype); } outDoc = mapper(outDoc); } diff --git a/src/long.ts b/src/long.ts index 77916fc5..b02d9b22 100644 --- a/src/long.ts +++ b/src/long.ts @@ -1,4 +1,5 @@ import { BSON_MAJOR_VERSION } from './constants'; +import { BSONError } from './error'; import type { EJSONOptions } from './extended_json'; import type { Timestamp } from './timestamp'; @@ -250,7 +251,7 @@ export class Long { * @returns The corresponding Long value */ static fromString(str: string, unsigned?: boolean, radix?: number): Long { - if (str.length === 0) throw Error('empty string'); + if (str.length === 0) throw new BSONError('empty string'); if (str === 'NaN' || str === 'Infinity' || str === '+Infinity' || str === '-Infinity') return Long.ZERO; if (typeof unsigned === 'number') { @@ -260,10 +261,10 @@ export class Long { unsigned = !!unsigned; } radix = radix || 10; - if (radix < 2 || 36 < radix) throw RangeError('radix'); + if (radix < 2 || 36 < radix) throw new BSONError('radix'); let p; - if ((p = str.indexOf('-')) > 0) throw Error('interior hyphen'); + if ((p = str.indexOf('-')) > 0) throw new BSONError('interior hyphen'); else if (p === 0) { return Long.fromString(str.substring(1), unsigned, radix).neg(); } @@ -431,7 +432,7 @@ export class Long { */ divide(divisor: string | number | Long | Timestamp): Long { if (!Long.isLong(divisor)) divisor = Long.fromValue(divisor); - if (divisor.isZero()) throw Error('division by zero'); + if (divisor.isZero()) throw new BSONError('division by zero'); // use wasm support if present if (wasm) { @@ -959,7 +960,7 @@ export class Long { */ toString(radix?: number): string { radix = radix || 10; - if (radix < 2 || 36 < radix) throw RangeError('radix'); + if (radix < 2 || 36 < radix) throw new BSONError('radix'); if (this.isZero()) return '0'; if (this.isNegative()) { // Unsigned Longs are never negative diff --git a/src/objectid.ts b/src/objectid.ts index ac83f9dc..012c1e19 100644 --- a/src/objectid.ts +++ b/src/objectid.ts @@ -1,5 +1,5 @@ import { BSON_MAJOR_VERSION } from './constants'; -import { BSONTypeError } from './error'; +import { BSONError } from './error'; import { isUint8Array } from './parser/utils'; import { BSONDataView, ByteUtils } from './utils/byte_utils'; @@ -57,9 +57,7 @@ export class ObjectId { let workingId; if (typeof inputId === 'object' && inputId && 'id' in inputId) { if (typeof inputId.id !== 'string' && !ArrayBuffer.isView(inputId.id)) { - throw new BSONTypeError( - 'Argument passed in must have an id that is of type string or Buffer' - ); + throw new BSONError('Argument passed in must have an id that is of type string or Buffer'); } if ('toHexString' in inputId && typeof inputId.toHexString === 'function') { workingId = ByteUtils.fromHex(inputId.toHexString()); @@ -85,17 +83,17 @@ export class ObjectId { if (bytes.byteLength === 12) { this[kId] = bytes; } else { - throw new BSONTypeError('Argument passed in must be a string of 12 bytes'); + throw new BSONError('Argument passed in must be a string of 12 bytes'); } } else if (workingId.length === 24 && checkForHexRegExp.test(workingId)) { this[kId] = ByteUtils.fromHex(workingId); } else { - throw new BSONTypeError( + throw new BSONError( 'Argument passed in must be a string of 12 bytes or a string of 24 hex characters or an integer' ); } } else { - throw new BSONTypeError('Argument passed in does not match the accepted types'); + throw new BSONError('Argument passed in does not match the accepted types'); } // If we are caching the hex string if (ObjectId.cacheHexString) { @@ -271,7 +269,7 @@ export class ObjectId { static createFromHexString(hexString: string): ObjectId { // Throw an error if it's not a valid setup if (typeof hexString === 'undefined' || (hexString != null && hexString.length !== 24)) { - throw new BSONTypeError( + throw new BSONError( 'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters' ); } diff --git a/src/parser/serializer.ts b/src/parser/serializer.ts index 04115d49..1ee93a23 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, BSONTypeError } from '../error'; +import { BSONError } from '../error'; import type { Int32 } from '../int_32'; import { Long } from '../long'; import type { MinKey } from '../min_key'; @@ -165,7 +165,7 @@ function serializeRegExp(buffer: Uint8Array, key: string, value: RegExp, index: index = index + numberOfWrittenBytes; buffer[index++] = 0; if (value.source && value.source.match(regexp) != null) { - throw Error('value ' + value.source + ' must not contain null bytes'); + throw new BSONError('value ' + value.source + ' must not contain null bytes'); } // Adjust the index index = index + ByteUtils.encodeUTF8Into(buffer, value.source, index); @@ -194,7 +194,7 @@ function serializeBSONRegExp(buffer: Uint8Array, key: string, value: BSONRegExp, if (value.pattern.match(regexp) != null) { // The BSON spec doesn't allow keys with null bytes because keys are // null-terminated. - throw Error('pattern ' + value.pattern + ' must not contain null bytes'); + throw new BSONError('pattern ' + value.pattern + ' must not contain null bytes'); } // Adjust the index @@ -241,7 +241,7 @@ function serializeObjectId(buffer: Uint8Array, key: string, value: ObjectId, ind if (isUint8Array(value.id)) { buffer.set(value.id.subarray(0, 12), index); } else { - throw new BSONTypeError('object [' + JSON.stringify(value) + '] is not a valid ObjectId'); + throw new BSONError('object [' + JSON.stringify(value) + '] is not a valid ObjectId'); } // Adjust index @@ -675,7 +675,7 @@ export function serializeInto( } else if (typeof value === 'number') { index = serializeNumber(buffer, key, value, index); } else if (typeof value === 'bigint') { - throw new BSONTypeError('Unsupported type BigInt, please use Decimal128'); + throw new BSONError('Unsupported type BigInt, please use Decimal128'); } else if (typeof value === 'boolean') { index = serializeBoolean(buffer, key, value, index); } else if (value instanceof Date || isDate(value)) { @@ -737,7 +737,7 @@ export function serializeInto( } else if (value._bsontype === 'MinKey' || value._bsontype === 'MaxKey') { index = serializeMinMax(buffer, key, value, index); } else if (typeof value._bsontype !== 'undefined') { - throw new BSONTypeError(`Unrecognized or invalid _bsontype: ${String(value._bsontype)}`); + throw new BSONError(`Unrecognized or invalid _bsontype: ${String(value._bsontype)}`); } } } else if (object instanceof Map || isMap(object)) { @@ -763,14 +763,14 @@ export function serializeInto( if (key.match(regexp) != null) { // The BSON spec doesn't allow keys with null bytes because keys are // null-terminated. - throw Error('key ' + key + ' must not contain null bytes'); + throw new BSONError('key ' + key + ' must not contain null bytes'); } if (checkKeys) { if ('$' === key[0]) { - throw Error('key ' + key + " must not start with '$'"); + throw new BSONError('key ' + key + " must not start with '$'"); } else if (~key.indexOf('.')) { - throw Error('key ' + key + " must not contain '.'"); + throw new BSONError('key ' + key + " must not contain '.'"); } } } @@ -780,7 +780,7 @@ export function serializeInto( } else if (type === 'number') { index = serializeNumber(buffer, key, value, index); } else if (type === 'bigint' || isBigInt64Array(value) || isBigUInt64Array(value)) { - throw new BSONTypeError('Unsupported type BigInt, please use Decimal128'); + throw new BSONError('Unsupported type BigInt, please use Decimal128'); } else if (type === 'boolean') { index = serializeBoolean(buffer, key, value, index); } else if (value instanceof Date || isDate(value)) { @@ -840,7 +840,7 @@ export function serializeInto( } else if (value._bsontype === 'MinKey' || value._bsontype === 'MaxKey') { index = serializeMinMax(buffer, key, value, index); } else if (typeof value._bsontype !== 'undefined') { - throw new BSONTypeError(`Unrecognized or invalid _bsontype: ${String(value._bsontype)}`); + throw new BSONError(`Unrecognized or invalid _bsontype: ${String(value._bsontype)}`); } } } else { @@ -848,7 +848,7 @@ export function serializeInto( // Provided a custom serialization method object = object.toBSON(); if (object != null && typeof object !== 'object') { - throw new BSONTypeError('toBSON function did not return an object'); + throw new BSONError('toBSON function did not return an object'); } } @@ -868,14 +868,14 @@ export function serializeInto( if (key.match(regexp) != null) { // The BSON spec doesn't allow keys with null bytes because keys are // null-terminated. - throw Error('key ' + key + ' must not contain null bytes'); + throw new BSONError('key ' + key + ' must not contain null bytes'); } if (checkKeys) { if ('$' === key[0]) { - throw Error('key ' + key + " must not start with '$'"); + throw new BSONError('key ' + key + " must not start with '$'"); } else if (~key.indexOf('.')) { - throw Error('key ' + key + " must not contain '.'"); + throw new BSONError('key ' + key + " must not contain '.'"); } } } @@ -885,7 +885,7 @@ export function serializeInto( } else if (type === 'number') { index = serializeNumber(buffer, key, value, index); } else if (type === 'bigint') { - throw new BSONTypeError('Unsupported type BigInt, please use Decimal128'); + throw new BSONError('Unsupported type BigInt, please use Decimal128'); } else if (type === 'boolean') { index = serializeBoolean(buffer, key, value, index); } else if (value instanceof Date || isDate(value)) { @@ -947,7 +947,7 @@ export function serializeInto( } else if (value._bsontype === 'MinKey' || value._bsontype === 'MaxKey') { index = serializeMinMax(buffer, key, value, index); } else if (typeof value._bsontype !== 'undefined') { - throw new BSONTypeError(`Unrecognized or invalid _bsontype: ${String(value._bsontype)}`); + throw new BSONError(`Unrecognized or invalid _bsontype: ${String(value._bsontype)}`); } } } diff --git a/src/regexp.ts b/src/regexp.ts index bffb0bf9..c40de2b0 100644 --- a/src/regexp.ts +++ b/src/regexp.ts @@ -1,5 +1,5 @@ import { BSON_MAJOR_VERSION } from './constants'; -import { BSONError, BSONTypeError } from './error'; +import { BSONError } from './error'; import type { EJSONOptions } from './extended_json'; function alphabetize(str: string): string { @@ -103,7 +103,7 @@ export class BSONRegExp { BSONRegExp.parseOptions(doc.$regularExpression.options) ); } - throw new BSONTypeError(`Unexpected BSONRegExp EJSON object form: ${JSON.stringify(doc)}`); + throw new BSONError(`Unexpected BSONRegExp EJSON object form: ${JSON.stringify(doc)}`); } /** @internal */ diff --git a/src/uuid_utils.ts b/src/uuid_utils.ts index 859d5cfd..37f0dcfd 100644 --- a/src/uuid_utils.ts +++ b/src/uuid_utils.ts @@ -1,4 +1,4 @@ -import { BSONTypeError } from './error'; +import { BSONError } from './error'; import { ByteUtils } from './utils/byte_utils'; // Validation regex for v4 uuid (validates with or without dashes) @@ -10,7 +10,7 @@ export const uuidValidateString = (str: string): boolean => export const uuidHexStringToBuffer = (hexString: string): Uint8Array => { if (!uuidValidateString(hexString)) { - throw new BSONTypeError( + throw new BSONError( 'UUID string representations must be a 32 or 36 character hex string (dashes excluded/included). Format: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" or "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx".' ); } diff --git a/test/node/error.test.ts b/test/node/error.test.ts new file mode 100644 index 00000000..126e7f2b --- /dev/null +++ b/test/node/error.test.ts @@ -0,0 +1,85 @@ +import { expect } from 'chai'; +import { loadESModuleBSON } from '../load_bson'; + +import { __isWeb__, BSONError } from '../register-bson'; + +const instanceOfChecksWork = !__isWeb__; + +describe('BSONError', function () { + describe('isBSONError()', () => { + it('returns true for BSONError instances', () => { + const error = new BSONError('ah!'); + if (instanceOfChecksWork) expect(error).to.be.instanceOf(BSONError); + expect(BSONError.isBSONError(error)).to.be.true; + }); + + it('returns true for BSONError created from another vm', async () => { + const { + exports: { BSONError: BSONErrorFromAnotherVM } + } = await loadESModuleBSON(); + const error = new BSONErrorFromAnotherVM('ah!'); + + // Instanceof here is false but the helper still returns true + if (instanceOfChecksWork) expect(error).to.not.be.instanceOf(BSONError); + expect(BSONError.isBSONError(error)).to.be.true; + }); + + it('returns true for for any object that meets the required API bsonError:true and name,message,stack properties', () => { + expect( + BSONError.isBSONError({ + bsonError: true, + name: 'BSONError', + message: 'ah!', + stack: 'at line X' + }) + ).to.be.true; + // The types can be wrong for name,message,stack. + // isBSONError does not access the stack getter to avoid triggering generating it + expect(BSONError.isBSONError({ bsonError: true, name: Symbol(), message: false, stack: 2 })) + .to.be.true; + }); + + it('returns false for objects that are almost shaped like BSONError', () => { + expect(BSONError.isBSONError({ bsonError: true })).to.be.false; + expect(BSONError.isBSONError({ bsonError: false })).to.be.false; + expect(BSONError.isBSONError({ bsonError: true, name: 'BSONError' })).to.be.false; + expect(BSONError.isBSONError({ bsonError: true, name: 'BSONError', message: 'ah!' })).to.be + .false; + expect( + BSONError.isBSONError({ + bsonError: false, + name: 'BSONError', + message: 'ah!', + stack: 'at line X' + }) + ).to.be.false; + }); + + it('returns false for nullish and non-object inputs', async () => { + expect(BSONError.isBSONError(null)).to.be.false; + expect(BSONError.isBSONError(undefined)).to.be.false; + expect(BSONError.isBSONError(3)).to.be.false; + expect(BSONError.isBSONError(true)).to.be.false; + expect(BSONError.isBSONError(new Function())).to.be.false; + }); + }); + + it('should evaluate true on instanceof BSONError and Error', function () { + const bsonErr = new BSONError('ah!'); + if (instanceOfChecksWork) { + expect(bsonErr instanceof BSONError).to.be.true; + expect(bsonErr instanceof Error).to.be.true; + expect(bsonErr).to.be.instanceOf(BSONError); + expect(bsonErr).to.be.instanceOf(Error); + } else { + expect(bsonErr).to.have.property('name', 'BSONError'); + expect(Object.getPrototypeOf(BSONError.prototype)).to.have.property('name', 'Error'); + } + }); + + it('should correctly set BSONError name and message properties', function () { + const bsonErr = new BSONError('This is a BSONError message'); + expect(bsonErr.name).equals('BSONError'); + expect(bsonErr.message).equals('This is a BSONError message'); + }); +}); diff --git a/test/node/error_test.js b/test/node/error_test.js deleted file mode 100644 index 2ba82add..00000000 --- a/test/node/error_test.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -const { __isWeb__ } = require('../register-bson'); -const BSON = require('../register-bson'); -const BSONTypeError = BSON.BSONTypeError; -const BSONError = BSON.BSONError; - -describe('BSONTypeError', function () { - it('should evaluate true on instanceof BSONTypeError and TypeError', function () { - const bsonTypeErr = new BSONTypeError(); - if (__isWeb__) { - expect(bsonTypeErr).to.have.property('name', 'BSONTypeError'); - expect(Object.getPrototypeOf(BSONTypeError.prototype)).to.have.property('name', 'TypeError'); - } else { - expect(bsonTypeErr instanceof BSONTypeError).to.be.true; - expect(bsonTypeErr).to.be.instanceOf(BSONTypeError); - expect(bsonTypeErr instanceof TypeError).to.be.true; - expect(bsonTypeErr).to.be.instanceOf(TypeError); - } - }); - - it('should correctly set BSONTypeError name and message properties', function () { - const bsonTypeErr = new BSONTypeError('This is a BSONTypeError message'); - expect(bsonTypeErr.name).equals('BSONTypeError'); - expect(bsonTypeErr.message).equals('This is a BSONTypeError message'); - }); -}); - -describe('BSONError', function () { - it('should evaluate true on instanceof BSONError and Error', function () { - const bsonErr = new BSONError(); - if (__isWeb__) { - expect(bsonErr).to.have.property('name', 'BSONError'); - expect(Object.getPrototypeOf(BSONError.prototype)).to.have.property('name', 'Error'); - } else { - expect(bsonErr instanceof BSONError).to.be.true; - expect(bsonErr instanceof Error).to.be.true; - expect(bsonErr).to.be.instanceOf(BSONError); - expect(bsonErr).to.be.instanceOf(Error); - } - }); - - it('should correctly set BSONError name and message properties', function () { - const bsonErr = new BSONError('This is a BSONError message'); - expect(bsonErr.name).equals('BSONError'); - expect(bsonErr.message).equals('This is a BSONError message'); - }); -}); diff --git a/test/node/exports.test.ts b/test/node/exports.test.ts index 204f327e..004bc616 100644 --- a/test/node/exports.test.ts +++ b/test/node/exports.test.ts @@ -23,7 +23,6 @@ const EXPECTED_EXPORTS = [ 'BSONRegExp', 'Decimal128', 'BSONError', - 'BSONTypeError', 'setInternalBufferSize', 'serialize', 'serializeWithBufferAndIndex',