Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(NODE-3948): Add error code to MongoSystemError #3149

Merged
merged 6 commits into from Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/error.ts
Expand Up @@ -625,6 +625,8 @@ export class MongoSystemError extends MongoError {
if (reason) {
this.reason = reason;
}

this.code = reason.error?.code;
}

get name(): string {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -62,6 +62,7 @@ export {
MongoServerError,
MongoServerSelectionError,
MongoSystemError,
MongoTailableCursorError,
MongoTopologyClosedError,
MongoTransactionError,
MongoWriteConcernError
Expand Down
136 changes: 98 additions & 38 deletions test/unit/error.test.js → 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);
nbbeeken marked this conversation as resolved.
Show resolved Hide resolved
});
});
}
}
});

describe('MongoError#constructor', () => {
it('should accept a string', function () {
Expand Down Expand Up @@ -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('');
Expand Down Expand Up @@ -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('');
Expand Down Expand Up @@ -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 => {
Expand Down