Skip to content

Commit d32bcfd

Browse files
authored
RSDK-12206 Fix session reset on reconnection with saved creds (#663)
1 parent a69cf20 commit d32bcfd

File tree

2 files changed

+184
-23
lines changed

2 files changed

+184
-23
lines changed

src/robot/client.spec.ts

Lines changed: 179 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
// @vitest-environment happy-dom
22

3-
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
3+
import {
4+
beforeEach,
5+
afterEach,
6+
describe,
7+
expect,
8+
it,
9+
vi,
10+
type MockInstance,
11+
} from 'vitest';
412
import type { Transport } from '@connectrpc/connect';
513
import { createRouterTransport } from '@connectrpc/connect';
614
import { RobotService } from '../gen/robot/v1/robot_connect';
@@ -17,19 +25,53 @@ vi.mock('../rpc', async () => {
1725
});
1826

1927
describe('RobotClient', () => {
20-
describe('event listeners', () => {
21-
let mockTransport: Transport;
28+
let mockTransport: Transport;
29+
let mockPeerConnection: RTCPeerConnection;
30+
let mockDataChannel: RTCDataChannel;
31+
let client: RobotClient;
32+
33+
beforeEach(() => {
34+
mockTransport = createRouterTransport(({ service }) => {
35+
service(RobotService, {
36+
resourceNames: () => ({ resources: [] }),
37+
getOperations: () => ({ operations: [] }),
38+
});
39+
});
40+
41+
mockPeerConnection = {
42+
close: vi.fn(),
43+
addEventListener: vi.fn(),
44+
removeEventListener: vi.fn(),
45+
iceConnectionState: 'connected',
46+
} as unknown as RTCPeerConnection;
47+
48+
mockDataChannel = {
49+
close: vi.fn(),
50+
addEventListener: vi.fn(),
51+
removeEventListener: vi.fn(),
52+
readyState: 'open',
53+
} as unknown as RTCDataChannel;
54+
55+
vi.mocked(rpcModule.dialWebRTC).mockResolvedValue({
56+
transport: mockTransport,
57+
peerConnection: mockPeerConnection,
58+
dataChannel: mockDataChannel,
59+
});
2260

23-
let mockPeerConnection: RTCPeerConnection;
61+
client = new RobotClient();
62+
});
63+
64+
afterEach(() => {
65+
vi.clearAllMocks();
66+
});
67+
68+
describe('event listeners', () => {
2469
let pcAddEventListenerSpy: ReturnType<typeof vi.fn>;
2570
let pcRemoveEventListenerSpy: ReturnType<typeof vi.fn>;
2671

27-
let mockDataChannel: RTCDataChannel;
2872
let dcAddEventListenerSpy: ReturnType<typeof vi.fn>;
2973
let dcRemoveEventListenerSpy: ReturnType<typeof vi.fn>;
3074

31-
let client: RobotClient;
32-
3375
beforeEach(() => {
3476
pcAddEventListenerSpy = vi.fn();
3577
pcRemoveEventListenerSpy = vi.fn();
@@ -50,24 +92,11 @@ describe('RobotClient', () => {
5092
readyState: 'open',
5193
} as unknown as RTCDataChannel;
5294

53-
mockTransport = createRouterTransport(({ service }) => {
54-
service(RobotService, {
55-
resourceNames: () => ({ resources: [] }),
56-
getOperations: () => ({ operations: [] }),
57-
});
58-
});
59-
6095
vi.mocked(rpcModule.dialWebRTC).mockResolvedValue({
6196
transport: mockTransport,
6297
peerConnection: mockPeerConnection,
6398
dataChannel: mockDataChannel,
6499
});
65-
66-
client = new RobotClient();
67-
});
68-
69-
afterEach(() => {
70-
vi.clearAllMocks();
71100
});
72101

73102
it.each([
@@ -231,4 +260,134 @@ describe('RobotClient', () => {
231260
expect(dcRemoveCalls.length).toBeGreaterThanOrEqual(1);
232261
});
233262
});
263+
264+
describe('session management on reconnection', () => {
265+
let mockResetFn: MockInstance<[], void>;
266+
267+
const testCredential = {
268+
authEntity: 'test-entity',
269+
type: 'api-key' as const,
270+
payload: 'test-payload',
271+
};
272+
273+
const differentCredential = {
274+
authEntity: 'different-entity',
275+
type: 'api-key' as const,
276+
payload: 'different-payload',
277+
};
278+
279+
const accessToken = {
280+
type: 'access-token' as const,
281+
payload: 'test-access-token',
282+
};
283+
284+
const differentAccessToken = {
285+
type: 'access-token' as const,
286+
payload: 'different-access-token',
287+
};
288+
289+
beforeEach(() => {
290+
// Spy on the SessionManager's reset method to verify conditional reset behavior
291+
// eslint-disable-next-line vitest/no-restricted-vi-methods, @typescript-eslint/dot-notation
292+
mockResetFn = vi.spyOn(client['sessionManager'], 'reset');
293+
});
294+
295+
afterEach(() => {
296+
mockResetFn.mockRestore();
297+
});
298+
299+
it('should reset session when connecting for the first time', async () => {
300+
await client.dial({
301+
host: 'test-host',
302+
signalingAddress: 'https://test.local',
303+
credentials: testCredential,
304+
disableSessions: false,
305+
noReconnect: true,
306+
});
307+
308+
expect(mockResetFn).toHaveBeenCalledTimes(1);
309+
});
310+
311+
it.each([
312+
{
313+
description:
314+
'should reset session when credentials change during reconnection',
315+
initialCreds: testCredential,
316+
disableSessions: false,
317+
reconnectCreds: differentCredential,
318+
},
319+
{
320+
description: 'should reset session when sessions are disabled',
321+
initialCreds: testCredential,
322+
disableSessions: true,
323+
reconnectCreds: testCredential,
324+
},
325+
{
326+
description:
327+
'should reset session when reconnecting with no saved credentials',
328+
initialCreds: undefined,
329+
disableSessions: false,
330+
reconnectCreds: undefined,
331+
},
332+
{
333+
description:
334+
'should reset session when access token changes during reconnection',
335+
initialCreds: accessToken,
336+
disableSessions: false,
337+
reconnectCreds: differentAccessToken,
338+
},
339+
])(
340+
'$description',
341+
async ({ initialCreds, disableSessions, reconnectCreds }) => {
342+
await client.dial({
343+
host: 'test-host',
344+
signalingAddress: 'https://test.local',
345+
credentials: initialCreds,
346+
disableSessions,
347+
noReconnect: true,
348+
});
349+
350+
mockResetFn.mockClear();
351+
352+
await client.connect({ creds: reconnectCreds });
353+
354+
expect(mockResetFn).toHaveBeenCalledTimes(1);
355+
}
356+
);
357+
358+
it.each([
359+
{
360+
description:
361+
'should NOT reset session when reconnecting with same credentials',
362+
initialCreds: testCredential,
363+
reconnectCreds: testCredential,
364+
},
365+
{
366+
description:
367+
'should NOT reset session when reconnecting without explicitly passing creds (uses savedCreds)',
368+
initialCreds: testCredential,
369+
reconnectCreds: undefined,
370+
},
371+
{
372+
description:
373+
'should NOT reset session when using access token and reconnecting with same token',
374+
initialCreds: accessToken,
375+
reconnectCreds: accessToken,
376+
},
377+
])('$description', async ({ initialCreds, reconnectCreds }) => {
378+
await client.dial({
379+
host: 'test-host',
380+
signalingAddress: 'https://test.local',
381+
credentials: initialCreds,
382+
disableSessions: false,
383+
noReconnect: true,
384+
});
385+
386+
mockResetFn.mockClear();
387+
388+
await client.connect({ creds: reconnectCreds });
389+
390+
expect(mockResetFn).not.toHaveBeenCalled();
391+
});
392+
});
234393
});

src/robot/client.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -720,10 +720,12 @@ export class RobotClient extends EventDispatcher implements Robot {
720720
}
721721

722722
/*
723-
* TODO(RSDK-887): no longer reset if we are reusing authentication material; otherwise our session
724-
* and authentication context will no longer match.
723+
* Only reset session if credentials have changed or if explicitly required;
724+
* otherwise our session and authentication context will no longer match.
725725
*/
726-
this.sessionManager.reset();
726+
if (!creds || creds !== this.savedCreds || this.sessionOptions.disabled) {
727+
this.sessionManager.reset();
728+
}
727729

728730
try {
729731
const opts: DialOptions = {

0 commit comments

Comments
 (0)