diff --git a/src/error.ts b/src/error.ts index cfd70f32aa..02170dcd87 100644 --- a/src/error.ts +++ b/src/error.ts @@ -625,6 +625,8 @@ export class MongoSystemError extends MongoError { if (reason) { this.reason = reason; } + + this.code = reason.error?.code; } get name(): string { diff --git a/src/index.ts b/src/index.ts index 27c8ac944c..0502109519 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,6 +62,7 @@ export { MongoServerError, MongoServerSelectionError, MongoSystemError, + MongoTailableCursorError, MongoTopologyClosedError, MongoTransactionError, MongoWriteConcernError diff --git a/test/unit/error.test.js b/test/unit/error.test.ts similarity index 75% rename from test/unit/error.test.js rename to test/unit/error.test.ts index 134d309aa8..2bdb5f8479 100644 --- a/test/unit/error.test.js +++ b/test/unit/error.test.ts @@ -1,53 +1,77 @@ -/* eslint no-empty: ["error", { "allowEmptyCatch": true }] */ -'use strict'; - -const expect = require('chai').expect; -const mock = require('../tools/mongodb-mock/index'); -const { getSymbolFrom } = require('../tools/utils'); -const { ReplSetFixture } = require('../tools/common'); -const { ns, isHello } = require('../../src/utils'); -const { Topology } = require('../../src/sdam/topology'); -const { - MongoNetworkError, - MongoWriteConcernError, +import { expect } from 'chai'; + +import { + PoolClosedError as MongoPoolClosedError, + WaitQueueTimeoutError as MongoWaitQueueTimeoutError +} from '../../src/cmap/errors'; +import { + isRetryableEndTransactionError, + isSDAMUnrecoverableError, + LEGACY_NOT_PRIMARY_OR_SECONDARY_ERROR_MESSAGE, + LEGACY_NOT_WRITABLE_PRIMARY_ERROR_MESSAGE, + MongoSystemError, + NODE_IS_RECOVERING_ERROR_MESSAGE +} from '../../src/error'; +import * as importsFromErrorSrc from '../../src/error'; +import { MongoError, + MongoNetworkError, + MongoParseError, MongoServerError, - MongoParseError -} = require('../../src/index'); -const { - LEGACY_NOT_WRITABLE_PRIMARY_ERROR_MESSAGE, - LEGACY_NOT_PRIMARY_OR_SECONDARY_ERROR_MESSAGE, - NODE_IS_RECOVERING_ERROR_MESSAGE, - isRetryableEndTransactionError, - isSDAMUnrecoverableError -} = require('../../src/error'); -const { - PoolClosedError: MongoPoolClosedError, - WaitQueueTimeoutError: MongoWaitQueueTimeoutError -} = require('../../src/cmap/errors'); + MongoWriteConcernError, + TopologyDescription +} from '../../src/index'; +import * as importsFromEntryPoint from '../../src/index'; +import { Topology, TopologyOptions } from '../../src/sdam/topology'; +import { isHello, ns, setDifference } from '../../src/utils'; +import { ReplSetFixture } from '../tools/common'; +import { cleanup } from '../tools/mongodb-mock/index'; +import { getSymbolFrom } from '../tools/utils'; describe('MongoErrors', () => { - // import errors as object - let errorClasses = Object.fromEntries( - Object.entries(require('../../src/index')).filter(([key]) => key.endsWith('Error')) + let errorClassesFromEntryPoint = Object.fromEntries( + Object.entries(importsFromEntryPoint).filter( + ([key, value]) => key.endsWith('Error') && value.toString().startsWith('class') + ) + ) as any; + errorClassesFromEntryPoint = { + ...errorClassesFromEntryPoint, + MongoPoolClosedError, + MongoWaitQueueTimeoutError + }; + + const errorClassesFromErrorSrc = Object.fromEntries( + Object.entries(importsFromErrorSrc).filter( + ([key, value]) => key.endsWith('Error') && value.toString().startsWith('class') + ) ); - errorClasses = { ...errorClasses, MongoPoolClosedError, MongoWaitQueueTimeoutError }; - for (const errorName in errorClasses) { - describe(errorName, () => { - it(`name should be read-only`, () => { + it('all defined errors should be public', () => { + expect( + setDifference(Object.keys(errorClassesFromEntryPoint), Object.keys(errorClassesFromErrorSrc)) + ).to.have.property('size', 3); + + expect( + setDifference(Object.keys(errorClassesFromErrorSrc), Object.keys(errorClassesFromEntryPoint)) + ).to.have.property('size', 0); + }); + + describe('error names should be read-only', () => { + for (const [errorName, errorClass] of Object.entries(errorClassesFromEntryPoint)) { + it(`${errorName} should be read-only`, () => { // Dynamically create error class with message - let error = new errorClasses[errorName]('generated by test'); + const error = new (errorClass as any)('generated by test', {}); // expect name property to be class name expect(error).to.have.property('name', errorName); try { error.name = 'renamed by test'; + // eslint-disable-next-line no-empty } catch (err) {} expect(error).to.have.property('name', errorName); }); - }); - } + } + }); describe('MongoError#constructor', () => { it('should accept a string', function () { @@ -89,6 +113,39 @@ describe('MongoErrors', () => { }); }); + describe('MongoSystemError#constructor', () => { + context('when the topology description contains an error code', () => { + it('contains the specified code as a top level property', () => { + const topologyDescription = { + error: { + code: 123 + } + } as TopologyDescription; + + const error = new MongoSystemError('something went wrong', topologyDescription); + expect(error).to.haveOwnProperty('code', 123); + }); + }); + + context('when the topology description does not contain an error code', () => { + it('contains the code as a top level property that is undefined', () => { + const topologyDescription = { error: {} } as TopologyDescription; + + const error = new MongoSystemError('something went wrong', topologyDescription); + expect(error).to.haveOwnProperty('code', undefined); + }); + }); + + context('when the topology description does not contain an error property', () => { + it('contains the code as a top level property that is undefined', () => { + const topologyDescription = {} as TopologyDescription; + + const error = new MongoSystemError('something went wrong', topologyDescription); + expect(error).to.haveOwnProperty('code', undefined); + }); + }); + }); + describe('#isRetryableEndTransactionError', function () { context('when the error has a RetryableWriteError label', function () { const error = new MongoNetworkError(''); @@ -202,7 +259,10 @@ describe('MongoErrors', () => { const errorWithOptionFalse = new MongoNetworkError('', { beforeHandshake: false }); expect(getSymbolFrom(errorWithOptionFalse, 'beforeHandshake', false)).to.be.a('symbol'); - const errorWithBadOption = new MongoNetworkError('', { beforeHandshake: 'not boolean' }); + const errorWithBadOption = new MongoNetworkError('', { + // @ts-expect-error: beforeHandshake must be a boolean value + beforeHandshake: 'not boolean' + }); expect(getSymbolFrom(errorWithBadOption, 'beforeHandshake', false)).to.be.an('undefined'); const errorWithoutOption = new MongoNetworkError(''); @@ -254,14 +314,14 @@ describe('MongoErrors', () => { }; before(() => (test = new ReplSetFixture())); - afterEach(() => mock.cleanup()); + afterEach(() => cleanup()); beforeEach(() => test.setup()); function makeAndConnectReplSet(cb) { let invoked = false; const replSet = new Topology( [test.primaryServer.hostAddress(), test.firstSecondaryServer.hostAddress()], - { replicaSet: 'rs' } + { replicaSet: 'rs' } as TopologyOptions ); replSet.once('error', err => {