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' ;
412import type { Transport } from '@connectrpc/connect' ;
513import { createRouterTransport } from '@connectrpc/connect' ;
614import { RobotService } from '../gen/robot/v1/robot_connect' ;
@@ -17,19 +25,53 @@ vi.mock('../rpc', async () => {
1725} ) ;
1826
1927describe ( '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} ) ;
0 commit comments