From 37613f1a726d330967012b0c007560db1cc79bd9 Mon Sep 17 00:00:00 2001 From: Warren James Date: Mon, 9 Dec 2024 15:24:46 -0500 Subject: [PATCH] feat(NODE-6605): add error message when invalidating primary (#4340) Co-authored-by: Bailey Pearson --- src/error.ts | 39 ++++- src/index.ts | 1 + src/sdam/server_description.ts | 3 + src/sdam/topology_description.ts | 17 +- .../rs/new_primary_new_electionid.json | 6 +- .../rs/new_primary_new_electionid.yml | 6 +- .../rs/new_primary_new_setversion.json | 6 +- .../rs/new_primary_new_setversion.yml | 6 +- .../rs/null_election_id-pre-6.0.json | 2 +- .../rs/primary_disconnect_electionid.json | 3 +- .../rs/primary_disconnect_electionid.yml | 3 +- .../rs/primary_disconnect_setversion.json | 3 +- .../rs/primary_disconnect_setversion.yml | 3 +- ...on_greaterthan_max_without_electionid.json | 3 +- ...ion_greaterthan_max_without_electionid.yml | 3 +- ...setversion_without_electionid-pre-6.0.json | 3 +- .../setversion_without_electionid-pre-6.0.yml | 3 +- ...setversion_without_electionid-pre-6.0.json | 6 +- ..._setversion_without_electionid-pre-6.0.yml | 6 +- .../rs/use_setversion_without_electionid.json | 6 +- .../rs/use_setversion_without_electionid.yml | 6 +- ...rver_discovery_and_monitoring.spec.test.ts | 153 ++++++++++-------- test/unit/index.test.ts | 1 + 23 files changed, 187 insertions(+), 101 deletions(-) diff --git a/src/error.ts b/src/error.ts index a3ae965b78d..8f0adb0c440 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,10 +1,10 @@ -import type { Document } from './bson'; +import type { Document, ObjectId } from './bson'; import { type ClientBulkWriteError, type ClientBulkWriteResult } from './operations/client_bulk_write/common'; import type { ServerType } from './sdam/common'; -import type { TopologyVersion } from './sdam/server_description'; +import type { ServerDescription, TopologyVersion } from './sdam/server_description'; import type { TopologyDescription } from './sdam/topology_description'; /** @public */ @@ -340,6 +340,41 @@ export class MongoRuntimeError extends MongoDriverError { } } +/** + * An error generated when a primary server is marked stale, never directly thrown + * + * @public + * @category Error + */ +export class MongoStalePrimaryError extends MongoRuntimeError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ + constructor( + serverDescription: ServerDescription, + maxSetVersion: number | null, + maxElectionId: ObjectId | null, + options?: { cause?: Error } + ) { + super( + `primary marked stale due to electionId/setVersion mismatch: server setVersion: ${serverDescription.setVersion}, server electionId: ${serverDescription.electionId}, topology setVersion: ${maxSetVersion}, topology electionId: ${maxElectionId}`, + options + ); + } + + override get name(): string { + return 'MongoStalePrimaryError'; + } +} + /** * An error generated when a batch command is re-executed after one of the commands in the batch * has failed diff --git a/src/index.ts b/src/index.ts index 03423532dbb..794bed95884 100644 --- a/src/index.ts +++ b/src/index.ts @@ -77,6 +77,7 @@ export { MongoServerClosedError, MongoServerError, MongoServerSelectionError, + MongoStalePrimaryError, MongoSystemError, MongoTailableCursorError, MongoTopologyClosedError, diff --git a/src/sdam/server_description.ts b/src/sdam/server_description.ts index a3b7c506ef6..903baefcd81 100644 --- a/src/sdam/server_description.ts +++ b/src/sdam/server_description.ts @@ -112,7 +112,10 @@ export class ServerDescription { this.minRoundTripTime = options?.minRoundTripTime ?? 0; this.lastUpdateTime = now(); this.lastWriteDate = hello?.lastWrite?.lastWriteDate ?? 0; + // NOTE: This actually builds the stack string instead of holding onto the getter and all its + // associated references. This is done to prevent a memory leak. this.error = options.error ?? null; + this.error?.stack; // TODO(NODE-2674): Preserve int64 sent from MongoDB this.topologyVersion = this.error?.topologyVersion ?? hello?.topologyVersion ?? null; this.setName = hello?.setName ?? null; diff --git a/src/sdam/topology_description.ts b/src/sdam/topology_description.ts index f171423f599..d4f7af11477 100644 --- a/src/sdam/topology_description.ts +++ b/src/sdam/topology_description.ts @@ -1,6 +1,6 @@ import { EJSON, type ObjectId } from '../bson'; import * as WIRE_CONSTANTS from '../cmap/wire_protocol/constants'; -import { type MongoError, MongoRuntimeError } from '../error'; +import { type MongoError, MongoRuntimeError, MongoStalePrimaryError } from '../error'; import { compareObjectId, shuffle } from '../utils'; import { ServerType, TopologyType } from './common'; import { ServerDescription } from './server_description'; @@ -400,7 +400,9 @@ function updateRsFromPrimary( // replace serverDescription with a default ServerDescription of type "Unknown" serverDescriptions.set( serverDescription.address, - new ServerDescription(serverDescription.address) + new ServerDescription(serverDescription.address, undefined, { + error: new MongoStalePrimaryError(serverDescription, maxSetVersion, maxElectionId) + }) ); return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId]; @@ -416,7 +418,9 @@ function updateRsFromPrimary( // this primary is stale, we must remove it serverDescriptions.set( serverDescription.address, - new ServerDescription(serverDescription.address) + new ServerDescription(serverDescription.address, undefined, { + error: new MongoStalePrimaryError(serverDescription, maxSetVersion, maxElectionId) + }) ); return [checkHasPrimary(serverDescriptions), setName, maxSetVersion, maxElectionId]; @@ -438,7 +442,12 @@ function updateRsFromPrimary( for (const [address, server] of serverDescriptions) { if (server.type === ServerType.RSPrimary && server.address !== serverDescription.address) { // Reset old primary's type to Unknown. - serverDescriptions.set(address, new ServerDescription(server.address)); + serverDescriptions.set( + address, + new ServerDescription(server.address, undefined, { + error: new MongoStalePrimaryError(serverDescription, maxSetVersion, maxElectionId) + }) + ); // There can only be one primary break; diff --git a/test/spec/server-discovery-and-monitoring/rs/new_primary_new_electionid.json b/test/spec/server-discovery-and-monitoring/rs/new_primary_new_electionid.json index 509720d445a..ec6e736d55c 100644 --- a/test/spec/server-discovery-and-monitoring/rs/new_primary_new_electionid.json +++ b/test/spec/server-discovery-and-monitoring/rs/new_primary_new_electionid.json @@ -76,7 +76,8 @@ "a:27017": { "type": "Unknown", "setName": null, - "electionId": null + "electionId": null, + "error": "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { "type": "RSPrimary", @@ -123,7 +124,8 @@ "a:27017": { "type": "Unknown", "setName": null, - "electionId": null + "electionId": null, + "error": "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { "type": "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/new_primary_new_electionid.yml b/test/spec/server-discovery-and-monitoring/rs/new_primary_new_electionid.yml index 5641cfda954..945930a8f5f 100644 --- a/test/spec/server-discovery-and-monitoring/rs/new_primary_new_electionid.yml +++ b/test/spec/server-discovery-and-monitoring/rs/new_primary_new_electionid.yml @@ -63,7 +63,8 @@ phases: [ "a:27017": { type: "Unknown", setName: , - electionId: + electionId: , + error: "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { type: "RSPrimary", @@ -100,7 +101,8 @@ phases: [ "a:27017": { type: "Unknown", setName: , - electionId: + electionId:, + error: "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { type: "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/new_primary_new_setversion.json b/test/spec/server-discovery-and-monitoring/rs/new_primary_new_setversion.json index 96533c61ee2..1db05cbd5a1 100644 --- a/test/spec/server-discovery-and-monitoring/rs/new_primary_new_setversion.json +++ b/test/spec/server-discovery-and-monitoring/rs/new_primary_new_setversion.json @@ -76,7 +76,8 @@ "a:27017": { "type": "Unknown", "setName": null, - "electionId": null + "electionId": null, + "error": "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { "type": "RSPrimary", @@ -123,7 +124,8 @@ "a:27017": { "type": "Unknown", "setName": null, - "electionId": null + "electionId": null, + "error": "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { "type": "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/new_primary_new_setversion.yml b/test/spec/server-discovery-and-monitoring/rs/new_primary_new_setversion.yml index f2697971125..c46e987927f 100644 --- a/test/spec/server-discovery-and-monitoring/rs/new_primary_new_setversion.yml +++ b/test/spec/server-discovery-and-monitoring/rs/new_primary_new_setversion.yml @@ -63,7 +63,8 @@ phases: [ "a:27017": { type: "Unknown", setName: , - electionId: + electionId:, + error: "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { type: "RSPrimary", @@ -100,7 +101,8 @@ phases: [ "a:27017": { type: "Unknown", setName: , - electionId: + electionId:, + error: "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { type: "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/null_election_id-pre-6.0.json b/test/spec/server-discovery-and-monitoring/rs/null_election_id-pre-6.0.json index 9e7ccc6e7f2..7261fbfc2a8 100644 --- a/test/spec/server-discovery-and-monitoring/rs/null_election_id-pre-6.0.json +++ b/test/spec/server-discovery-and-monitoring/rs/null_election_id-pre-6.0.json @@ -66,7 +66,7 @@ "$oid": "000000000000000000000002" }, "minWireVersion": 0, - "maxWireVersion": 21 + "maxWireVersion": 7 } ] ], diff --git a/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_electionid.json b/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_electionid.json index 5a91188ea8d..d87f7a3612d 100644 --- a/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_electionid.json +++ b/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_electionid.json @@ -48,7 +48,8 @@ "a:27017": { "type": "Unknown", "setName": null, - "electionId": null + "electionId": null, + "error": "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { "type": "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_electionid.yml b/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_electionid.yml index 391ec31213f..7a2b9572093 100644 --- a/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_electionid.yml +++ b/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_electionid.yml @@ -36,7 +36,8 @@ phases: [ "a:27017": { type: "Unknown", setName: , - electionId: + electionId:, + error: "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { type: "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_setversion.json b/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_setversion.json index f7417ad77bc..0a59fd9ce62 100644 --- a/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_setversion.json +++ b/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_setversion.json @@ -48,7 +48,8 @@ "a:27017": { "type": "Unknown", "setName": null, - "electionId": null + "electionId": null, + "error": "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { "type": "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_setversion.yml b/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_setversion.yml index 57eeb573e48..1592dfff1a5 100644 --- a/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_setversion.yml +++ b/test/spec/server-discovery-and-monitoring/rs/primary_disconnect_setversion.yml @@ -36,7 +36,8 @@ phases: [ "a:27017": { type: "Unknown", setName: , - electionId: + electionId:, + error: "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { type: "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/setversion_greaterthan_max_without_electionid.json b/test/spec/server-discovery-and-monitoring/rs/setversion_greaterthan_max_without_electionid.json index 97870d71d53..e3451659472 100644 --- a/test/spec/server-discovery-and-monitoring/rs/setversion_greaterthan_max_without_electionid.json +++ b/test/spec/server-discovery-and-monitoring/rs/setversion_greaterthan_max_without_electionid.json @@ -65,7 +65,8 @@ "a:27017": { "type": "Unknown", "setName": null, - "electionId": null + "electionId": null, + "error": "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { "type": "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/setversion_greaterthan_max_without_electionid.yml b/test/spec/server-discovery-and-monitoring/rs/setversion_greaterthan_max_without_electionid.yml index 3252e0f6114..07b98e8af53 100644 --- a/test/spec/server-discovery-and-monitoring/rs/setversion_greaterthan_max_without_electionid.yml +++ b/test/spec/server-discovery-and-monitoring/rs/setversion_greaterthan_max_without_electionid.yml @@ -61,7 +61,8 @@ phases: [ "a:27017": { type: "Unknown", setName: , - electionId: + electionId:, + error: "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { type: "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/setversion_without_electionid-pre-6.0.json b/test/spec/server-discovery-and-monitoring/rs/setversion_without_electionid-pre-6.0.json index e62c6963ed3..6b6cd39255c 100644 --- a/test/spec/server-discovery-and-monitoring/rs/setversion_without_electionid-pre-6.0.json +++ b/test/spec/server-discovery-and-monitoring/rs/setversion_without_electionid-pre-6.0.json @@ -65,7 +65,8 @@ "a:27017": { "type": "Unknown", "setName": null, - "electionId": null + "electionId": null, + "error": "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { "type": "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/setversion_without_electionid-pre-6.0.yml b/test/spec/server-discovery-and-monitoring/rs/setversion_without_electionid-pre-6.0.yml index 0fe6819aa71..c186dfe3744 100644 --- a/test/spec/server-discovery-and-monitoring/rs/setversion_without_electionid-pre-6.0.yml +++ b/test/spec/server-discovery-and-monitoring/rs/setversion_without_electionid-pre-6.0.yml @@ -61,7 +61,8 @@ phases: [ "a:27017": { type: "Unknown", setName: , - electionId: + electionId:, + error: "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { type: "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid-pre-6.0.json b/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid-pre-6.0.json index 2f9b567b850..50c6666ee53 100644 --- a/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid-pre-6.0.json +++ b/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid-pre-6.0.json @@ -73,7 +73,8 @@ "a:27017": { "type": "Unknown", "setName": null, - "electionId": null + "electionId": null, + "error": "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { "type": "RSPrimary", @@ -117,7 +118,8 @@ "a:27017": { "type": "Unknown", "setName": null, - "electionId": null + "electionId": null, + "error": "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { "type": "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid-pre-6.0.yml b/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid-pre-6.0.yml index 24d6accbe0a..799075c1999 100644 --- a/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid-pre-6.0.yml +++ b/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid-pre-6.0.yml @@ -62,7 +62,8 @@ phases: [ "a:27017": { type: "Unknown", setName: , - electionId: + electionId:, + error: "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { type: "RSPrimary", @@ -99,7 +100,8 @@ phases: [ "a:27017": { type: "Unknown", setName: , - electionId: + electionId:, + error: "primary marked stale due to electionId/setVersion mismatch" }, "b:27017": { type: "RSPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid.json b/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid.json index 551f3e12c27..eaf586d7284 100644 --- a/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid.json +++ b/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid.json @@ -81,7 +81,8 @@ "b:27017": { "type": "Unknown", "setName": null, - "electionId": null + "electionId": null, + "error": "primary marked stale due to electionId/setVersion mismatch" } }, "topologyType": "ReplicaSetWithPrimary", @@ -128,7 +129,8 @@ "b:27017": { "type": "Unknown", "setName": null, - "electionId": null + "electionId": null, + "error": "primary marked stale due to electionId/setVersion mismatch" } }, "topologyType": "ReplicaSetWithPrimary", diff --git a/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid.yml b/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid.yml index 68c88bc503b..5359a1f67b5 100644 --- a/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid.yml +++ b/test/spec/server-discovery-and-monitoring/rs/use_setversion_without_electionid.yml @@ -68,7 +68,8 @@ phases: [ "b:27017": { type: "Unknown", setName: , - electionId: + electionId:, + error: "primary marked stale due to electionId/setVersion mismatch" } }, topologyType: "ReplicaSetWithPrimary", @@ -106,7 +107,8 @@ phases: [ "b:27017":{ type: "Unknown", setName: , - electionId: + electionId:, + error: "primary marked stale due to electionId/setVersion mismatch" } }, topologyType: "ReplicaSetWithPrimary", diff --git a/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts b/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts index 9ba97bf8fd6..be7ff7ef458 100644 --- a/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts +++ b/test/unit/assorted/server_discovery_and_monitoring.spec.test.ts @@ -10,6 +10,7 @@ import { isRecord, MongoClient, MongoCompatibilityError, + MongoError, MongoNetworkError, MongoNetworkTimeoutError, MongoServerError, @@ -25,6 +26,7 @@ import { ServerHeartbeatStartedEvent, ServerHeartbeatSucceededEvent, ServerOpeningEvent, + squashError, Topology, TOPOLOGY_CLOSED, TOPOLOGY_DESCRIPTION_CHANGED, @@ -108,6 +110,7 @@ interface MonitoringOutcome { interface OutcomeServerDescription { type?: string; setName?: string; + error?: { message: string }; setVersion?: number; electionId?: ObjectId | null; logicalSessionTimeoutMinutes?: number; @@ -322,84 +325,88 @@ async function executeSDAMTest(testData: SDAMTest) { // connect the topology await client.connect(); - for (const phase of testData.phases) { - // Determine which of the two kinds of phases we're running - if ('responses' in phase && phase.responses != null) { - // phase with responses for hello simulations - for (const [address, hello] of phase.responses) { - client.topology.serverUpdateHandler(new ServerDescription(address, hello)); - } - } else if ('applicationErrors' in phase && phase.applicationErrors) { - // phase with applicationErrors simulating error's from network, timeouts, server - for (const appError of phase.applicationErrors) { - // Stub will return appError to SDAM machinery - const checkOutStub = sinon - .stub(ConnectionPool.prototype, 'checkOut') - .callsFake(checkoutStubImpl(appError)); - - const server = client.topology.s.servers.get(appError.address); - - // Run a dummy command to encounter the error - const res = server.command.bind(server)(ns('admin.$cmd'), { ping: 1 }, {}); - const thrownError = await res.catch(error => error); - - // Restore the stub before asserting anything in case of errors - checkOutStub.restore(); - - const isApplicationError = error => { - // These errors all come from the withConnection stub - return ( - error instanceof MongoNetworkError || - error instanceof MongoNetworkTimeoutError || - error instanceof MongoServerError - ); - }; - expect( - thrownError, - `expected the error thrown to be one of MongoNetworkError, MongoNetworkTimeoutError or MongoServerError (referred to in the spec as an "Application Error") got ${thrownError.name} ${thrownError.stack}` - ).to.satisfy(isApplicationError); + try { + for (const phase of testData.phases) { + // Determine which of the two kinds of phases we're running + if ('responses' in phase && phase.responses != null) { + // phase with responses for hello simulations + for (const [address, hello] of phase.responses) { + client.topology.serverUpdateHandler(new ServerDescription(address, hello)); + } + } else if ('applicationErrors' in phase && phase.applicationErrors) { + // phase with applicationErrors simulating error's from network, timeouts, server + for (const appError of phase.applicationErrors) { + // Stub will return appError to SDAM machinery + const checkOutStub = sinon + .stub(ConnectionPool.prototype, 'checkOut') + .callsFake(checkoutStubImpl(appError)); + + const server = client.topology.s.servers.get(appError.address); + + // Run a dummy command to encounter the error + const res = server.command.bind(server)(ns('admin.$cmd'), { ping: 1 }, {}); + const thrownError = await res.catch(error => error); + + // Restore the stub before asserting anything in case of errors + checkOutStub.restore(); + + const isApplicationError = error => { + // These errors all come from the withConnection stub + return ( + error instanceof MongoNetworkError || + error instanceof MongoNetworkTimeoutError || + error instanceof MongoServerError + ); + }; + expect( + thrownError, + `expected the error thrown to be one of MongoNetworkError, MongoNetworkTimeoutError or MongoServerError (referred to in the spec as an "Application Error") got ${thrownError.name} ${thrownError.stack}` + ).to.satisfy(isApplicationError); + } + } else if (phase.outcome != null && Object.keys(phase).length === 1) { + // Load Balancer SDAM tests have no "work" to be done for the phase + } else { + expect.fail(ejson`Unknown phase shape - ${phase}`); } - } else if (phase.outcome != null && Object.keys(phase).length === 1) { - // Load Balancer SDAM tests have no "work" to be done for the phase - } else { - expect.fail(ejson`Unknown phase shape - ${phase}`); - } - if ('outcome' in phase && phase.outcome != null) { - if (isMonitoringOutcome(phase.outcome)) { - // Test for monitoring events - const expectedEvents = convertOutcomeEvents(phase.outcome.events); + if ('outcome' in phase && phase.outcome != null) { + if (isMonitoringOutcome(phase.outcome)) { + // Test for monitoring events + const expectedEvents = convertOutcomeEvents(phase.outcome.events); - expect(events).to.have.length(expectedEvents.length); - for (const [i, actualEvent] of Object.entries(events)) { - const actualEventClone = cloneForCompare(actualEvent); - expect(actualEventClone).to.matchMongoSpec(expectedEvents[i]); - } - } else if (isTopologyDescriptionOutcome(phase.outcome)) { - // Test for SDAM machinery correctly changing the topology type among other properties - assertTopologyDescriptionOutcomeExpectations(client.topology, phase.outcome); - if (phase.outcome.compatible === false) { - // driver specific error throwing - if (testData.description === 'Multiple mongoses with large minWireVersion') { - // TODO(DRIVERS-2250): There is test bug that causes two errors - // this will start failing when the test is synced and fixed - expect(errorsThrown).to.have.lengthOf(2); + expect(events).to.have.length(expectedEvents.length); + for (const [i, actualEvent] of Object.entries(events)) { + const actualEventClone = cloneForCompare(actualEvent); + expect(actualEventClone).to.matchMongoSpec(expectedEvents[i]); + } + } else if (isTopologyDescriptionOutcome(phase.outcome)) { + // Test for SDAM machinery correctly changing the topology type among other properties + assertTopologyDescriptionOutcomeExpectations(client.topology, phase.outcome); + if (phase.outcome.compatible === false) { + // driver specific error throwing + if (testData.description === 'Multiple mongoses with large minWireVersion') { + // TODO(DRIVERS-2250): There is test bug that causes two errors + // this will start failing when the test is synced and fixed + expect(errorsThrown).to.have.lengthOf(2); + } else { + expect(errorsThrown).to.have.lengthOf(1); + } + expect(errorsThrown[0]).to.be.instanceOf(MongoCompatibilityError); + expect(errorsThrown[0].message).to.match(/but this version of the driver/); } else { - expect(errorsThrown).to.have.lengthOf(1); + // unset or true means no errors should be thrown + expect(errorsThrown).to.be.empty; } - expect(errorsThrown[0]).to.be.instanceOf(MongoCompatibilityError); - expect(errorsThrown[0].message).to.match(/but this version of the driver/); } else { - // unset or true means no errors should be thrown - expect(errorsThrown).to.be.empty; + expect.fail(ejson`Unknown outcome shape - ${phase.outcome}`); } - } else { - expect.fail(ejson`Unknown outcome shape - ${phase.outcome}`); - } - events = []; - errorsThrown = []; + events = []; + errorsThrown = []; + } } + } finally { + await client.close().catch(squashError); } } @@ -464,8 +471,14 @@ function assertTopologyDescriptionOutcomeExpectations( if (WIRE_VERSION_KEYS.has(expectedKey) && expectedValue === null) { // For wireVersion keys we default to zero instead of null expect(actualServer).to.have.property(expectedKey, 0); - } else { + } else if (expectedKey !== 'error') { expect(actualServer).to.have.deep.property(expectedKey, expectedValue); + } else { + expect(typeof expectedValue).to.equal('string'); + expect(actualServer) + .to.have.property(expectedKey) + .instanceof(MongoError) + .to.match(new RegExp(expectedValue as string)); } } } diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 7b064b1078d..f021448cd55 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -106,6 +106,7 @@ const EXPECTED_EXPORTS = [ 'MongoTailableCursorError', 'MongoTopologyClosedError', 'MongoTransactionError', + 'MongoStalePrimaryError', 'MongoUnexpectedServerResponseError', 'MongoWriteConcernError', 'ObjectId',