@@ -19,17 +19,22 @@ package io.getstream.video.android.core.rtc
1919import android.graphics.ColorSpace.match
2020import android.os.PowerManager
2121import androidx.lifecycle.Lifecycle
22+ import io.getstream.android.video.generated.models.OwnCapability
2223import io.getstream.video.android.core.Call
2324import io.getstream.video.android.core.CallState
2425import io.getstream.video.android.core.MediaManagerImpl
26+ import io.getstream.video.android.core.ParticipantState
2527import io.getstream.video.android.core.StreamVideo
2628import io.getstream.video.android.core.StreamVideoClient
2729import io.getstream.video.android.core.call.RtcSession
2830import io.getstream.video.android.core.call.connection.Publisher
2931import io.getstream.video.android.core.events.ICETrickleEvent
32+ import io.getstream.video.android.core.events.JoinCallResponseEvent
3033import io.getstream.video.android.core.events.SubscriberOfferEvent
3134import io.getstream.video.android.core.internal.module.SfuConnectionModule
3235import io.getstream.video.android.core.model.IceServer
36+ import io.getstream.video.android.core.socket.sfu.SfuSocketConnection
37+ import io.getstream.video.android.core.socket.sfu.state.SfuSocketState
3338import io.mockk.MockKAnnotations
3439import io.mockk.coEvery
3540import io.mockk.coJustRun
@@ -40,26 +45,36 @@ import io.mockk.impl.annotations.RelaxedMockK
4045import io.mockk.mockk
4146import io.mockk.spyk
4247import io.mockk.unmockkAll
48+ import io.mockk.verify
4349import junit.framework.TestCase.assertEquals
4450import junit.framework.TestCase.assertNotNull
4551import junit.framework.TestCase.assertNull
4652import junit.framework.TestCase.assertTrue
53+ import kotlinx.coroutines.delay
54+ import kotlinx.coroutines.flow.MutableStateFlow
55+ import kotlinx.coroutines.launch
4756import kotlinx.coroutines.test.StandardTestDispatcher
4857import kotlinx.coroutines.test.TestScope
4958import kotlinx.coroutines.test.UnconfinedTestDispatcher
5059import kotlinx.coroutines.test.runTest
5160import org.junit.After
5261import org.junit.Before
53- import org.junit.Ignore
5462import org.junit.Test
5563import org.webrtc.SessionDescription
64+ import stream.video.sfu.models.ParticipantCount
5665import stream.video.sfu.models.PeerType
66+ import stream.video.sfu.models.PublishOption
67+ import stream.video.sfu.models.TrackType
68+ import stream.video.sfu.models.VideoDimension
5769import stream.video.sfu.signal.SendAnswerResponse
5870
5971class RtcSessionTest2 {
6072
6173 private val testDispatcher = StandardTestDispatcher ()
62- private val testScope = TestScope (UnconfinedTestDispatcher ())
74+ private val testScope = TestScope (testDispatcher)
75+ private val ownCapabilitiesFlow = MutableStateFlow <List <OwnCapability >>(emptyList())
76+ private val participantsFlow = MutableStateFlow <List <ParticipantState >>(emptyList())
77+ private val remoteParticipantsFlow = MutableStateFlow <List <ParticipantState >>(emptyList())
6378
6479 @MockK
6580 private lateinit var mockPowerManager: PowerManager
@@ -99,6 +114,10 @@ class RtcSessionTest2 {
99114 )
100115 } returns mockk(relaxed = true ) {}
101116 }
117+ every { mockCallState.ownCapabilities } returns ownCapabilitiesFlow
118+ every { mockCallState.participants } returns participantsFlow
119+ every { mockCallState.remoteParticipants } returns remoteParticipantsFlow
120+ every { mockCallState.replaceParticipants(any()) } answers { }
102121
103122 // We can stub out other pieces
104123 every { mockCallState.me.value } returns null
@@ -255,10 +274,17 @@ class RtcSessionTest2 {
255274 }
256275
257276 // TODO: Test is broken because socket connection is not established in this test.
258- @Ignore
259277 @Test
260278 fun `handleIceTrickle adds event to publisherPendingEvents if publisher is null` () = runTest {
261279 // Given an RtcSession with no publisher set (publisher = null by default until fully joined)
280+ val mockSocket = mockk<SfuSocketConnection >()
281+ val mockConnectedEvent = mockk<JoinCallResponseEvent >(relaxed = true )
282+ val socketStateFlow =
283+ MutableStateFlow <SfuSocketState >(SfuSocketState .Connected (mockConnectedEvent))
284+ every { mockSocket.state() } returns socketStateFlow
285+ val mockModule = mockk<SfuConnectionModule >(relaxed = true ) {
286+ every { socketConnection } returns mockSocket
287+ }
262288 val rtcSession = RtcSession (
263289 client = mockStreamVideo,
264290 powerManager = mockPowerManager,
@@ -271,8 +297,9 @@ class RtcSessionTest2 {
271297 sfuToken = " fake-sfu-token" ,
272298 clientImpl = mockVideoClient,
273299 coroutineScope = testScope,
300+ rtcSessionScope = testScope,
274301 remoteIceServers = emptyList(),
275- sfuConnectionModuleProvider = { mockk(relaxed = true ) },
302+ sfuConnectionModuleProvider = { mockModule },
276303 )
277304 // Confirm publisher is null
278305 assertNull(rtcSession.publisher)
@@ -290,6 +317,7 @@ class RtcSessionTest2 {
290317
291318 // When
292319 rtcSession.handleIceTrickle(event)
320+ testScope.testScheduler.advanceUntilIdle()
293321
294322 // Then
295323 // The event should be added to publisherPendingEvents
@@ -380,9 +408,94 @@ class RtcSessionTest2 {
380408 coVerify { publisher.close(any()) }
381409 }
382410
411+ @Test
412+ fun `join response without publish capability skips publisher creation` () = runTest {
413+ ownCapabilitiesFlow.value = emptyList()
414+ val (rtcSession, _) = createRtcSessionSpyWithMockSocket()
415+ val event = fakeJoinResponseEvent(samplePublishOptions())
416+
417+ rtcSession.handleEvent(event)
418+ testScope.testScheduler.advanceUntilIdle()
419+
420+ assertNull(rtcSession.publisher)
421+ verify(exactly = 0 ) { rtcSession[" createPublisher" ](any<List <PublishOption >>()) }
422+ }
423+
383424 private fun <T > RtcSession.fieldValue (name : String ): T ? {
384425 val field = RtcSession ::class .java.getDeclaredField(name)
385426 field.isAccessible = true
386427 return field.get(this ) as ? T
387428 }
429+
430+ private fun samplePublishOptions (): List <PublishOption > = listOf (
431+ PublishOption (
432+ track_type = TrackType .TRACK_TYPE_VIDEO ,
433+ bitrate = 1_000_000 ,
434+ fps = 30 ,
435+ max_spatial_layers = 1 ,
436+ max_temporal_layers = 1 ,
437+ video_dimension = VideoDimension (width = 1280 , height = 720 ),
438+ id = 1 ,
439+ ),
440+ )
441+
442+ private fun fakeJoinResponseEvent (
443+ publishOptions : List <PublishOption >,
444+ ): JoinCallResponseEvent {
445+ val protoCallState = mockk< stream.video.sfu.models.CallState > (relaxed = true ) {
446+ every { participants } returns emptyList()
447+ }
448+ val count = mockk< io.getstream.video.android.core.events.ParticipantCount > (relaxed = true )
449+ return mockk(relaxed = true ) {
450+ every { callState } returns protoCallState
451+ every { participantCount } returns count
452+ every { fastReconnectDeadlineSeconds } returns 0
453+ every { isReconnected } returns false
454+ every { this @mockk.publishOptions } returns publishOptions
455+ }
456+ }
457+
458+ private fun createRtcSessionSpyWithMockSocket (): Pair <RtcSession , Publisher > {
459+ val mockSocket = mockk<SfuSocketConnection >()
460+ val mockConnectedEvent = mockk<JoinCallResponseEvent >(relaxed = true )
461+ val socketStateFlow =
462+ MutableStateFlow <SfuSocketState >(SfuSocketState .Connected (mockConnectedEvent))
463+ every { mockSocket.state() } returns socketStateFlow
464+ every { mockSocket.whenConnected(any<Long >(), any()) } answers {
465+ val callback = secondArg< suspend (String ) -> Unit > ()
466+ // Launch the callback in backgroundScope to simulate the real whenConnected behavior
467+ // The real impl launches in socket's scope; we launch in background scope for control
468+ testScope.backgroundScope.launch {
469+ delay(500 ) // Simulate the real whenConnected delay
470+ callback(" connection-id" )
471+ }
472+ Unit // Return Unit since whenConnected returns Unit
473+ }
474+ val mockModule = mockk<SfuConnectionModule >(relaxed = true ) {
475+ every { socketConnection } returns mockSocket
476+ every { api } returns mockk(relaxed = true )
477+ }
478+ val rtcSession = spyk(
479+ RtcSession (
480+ client = mockStreamVideo,
481+ powerManager = mockPowerManager,
482+ call = mockCall,
483+ sessionId = " session-id" ,
484+ apiKey = " api-key" ,
485+ lifecycle = mockLifecycle,
486+ sfuUrl = " https://test-sfu.stream.com" ,
487+ sfuWsUrl = " wss://test-sfu.stream.com" ,
488+ sfuToken = " fake-sfu-token" ,
489+ clientImpl = mockVideoClient,
490+ coroutineScope = testScope,
491+ rtcSessionScope = testScope,
492+ remoteIceServers = emptyList(),
493+ sfuConnectionModuleProvider = { mockModule },
494+ ),
495+ recordPrivateCalls = true ,
496+ )
497+ val publisherMock = mockk<Publisher >(relaxed = true )
498+ every { rtcSession[" createPublisher" ](any<List <PublishOption >>()) } returns publisherMock
499+ return rtcSession to publisherMock
500+ }
388501}
0 commit comments