diff --git a/packages/devtools-connect/src/connect.spec.ts b/packages/devtools-connect/src/connect.spec.ts index 7adf1e0c..91a65b0a 100644 --- a/packages/devtools-connect/src/connect.spec.ts +++ b/packages/devtools-connect/src/connect.spec.ts @@ -10,6 +10,7 @@ import sinonChai from 'sinon-chai'; import { Agent as HTTPSAgent } from 'https'; import { MongoCluster } from 'mongodb-runner'; import { tmpdir } from 'os'; +import * as devtoolsProxySupport from '@mongodb-js/devtools-proxy-support'; chai.use(sinonChai); @@ -530,6 +531,79 @@ describe('devtools connect', function () { ); }); }); + + describe('tunnel error events', function () { + let originalDescriptor: PropertyDescriptor; + let fakeTunnel: EventEmitter & { + listen: sinon.SinonStub; + close: sinon.SinonStub; + config: any; + logger: EventEmitter; + }; + + beforeEach(function () { + const tunnel = Object.assign(new EventEmitter(), { + listen: sinon.stub().resolves(), + close: sinon.stub().resolves(), + config: { + proxyHost: '127.0.0.1', + proxyPort: 1080, + proxyUsername: 'u', + proxyPassword: 'p', + }, + logger: new EventEmitter(), + }); + fakeTunnel = tunnel as any; + + // devtools-proxy-support exports createSocks5Tunnel as a non-writable + // getter, so sinon.stub() can't replace it. Use Object.defineProperty + // directly to inject the fake tunnel factory. + originalDescriptor = Object.getOwnPropertyDescriptor( + devtoolsProxySupport, + 'createSocks5Tunnel', + )!; + Object.defineProperty(devtoolsProxySupport, 'createSocks5Tunnel', { + configurable: true, + writable: true, + value: () => fakeTunnel, + }); + }); + + afterEach(function () { + Object.defineProperty( + devtoolsProxySupport, + 'createSocks5Tunnel', + originalDescriptor, + ); + }); + + for (const [tunnelEvent, busEvent] of [ + ['error', 'devtools-connect:tunnel-error'], + ['forwardingError', 'devtools-connect:tunnel-forwarding-error'], + ] as const) { + it(`emits ${busEvent} when tunnel emits '${tunnelEvent}'`, async function () { + const mClient = stubConstructor(FakeMongoClient); + const mClientType = sinon.stub().returns(mClient); + mClient.connect.resolves(mClient); + + const { client } = await connectMongoClient( + 'localhost:27017', + { ...defaultOpts, proxy: { proxy: 'socks5://localhost:1080' } }, + bus, + mClientType as any, + ); + + const received: any[] = []; + bus.on(busEvent, (ev) => received.push(ev)); + fakeTunnel.emit(tunnelEvent, new Error(`test ${tunnelEvent}`)); + + expect(received).to.have.lengthOf(1); + expect(received[0].error.message).to.equal(`test ${tunnelEvent}`); + + mClient.emit('close'); + }); + } + }); }); describe('integration', function () { diff --git a/packages/devtools-connect/src/connect.ts b/packages/devtools-connect/src/connect.ts index 04705317..9ec14d6e 100644 --- a/packages/devtools-connect/src/connect.ts +++ b/packages/devtools-connect/src/connect.ts @@ -555,6 +555,14 @@ async function connectMongoClientImpl({ 'mongodb://', ); cleanupOnClientClose.push(() => tunnel?.close()); + tunnel?.on('error', (err) => { + logger.emit('devtools-connect:tunnel-error', { error: err }); + }); + tunnel?.on('forwardingError', (err) => { + logger.emit('devtools-connect:tunnel-forwarding-error', { + error: err, + }); + }); } for (const proxyLogger of new Set([ tunnel?.logger, diff --git a/packages/devtools-connect/src/types.ts b/packages/devtools-connect/src/types.ts index 0e2b9d93..9c942fa7 100644 --- a/packages/devtools-connect/src/types.ts +++ b/packages/devtools-connect/src/types.ts @@ -103,6 +103,10 @@ export interface ConnectEventMap 'devtools-connect:retry-after-tls-error': ( ev: ConnectRetryAfterTLSErrorEvent, ) => void; + /** Signals that the proxy tunnel encountered a session-level error (e.g. SSH connection lost) */ + 'devtools-connect:tunnel-error': (ev: { error: Error }) => void; + /** Signals that the proxy tunnel failed to forward a connection */ + 'devtools-connect:tunnel-forwarding-error': (ev: { error: Error }) => void; } export type ConnectEventArgs =