Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions packages/devtools-connect/src/connect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 () {
Expand Down
8 changes: 8 additions & 0 deletions packages/devtools-connect/src/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions packages/devtools-connect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<K extends keyof ConnectEventMap> =
Expand Down
Loading