11package com.flipcash.shared.chat
22
3+ import androidx.lifecycle.LifecycleOwner
34import com.flipcash.app.core.dispatchers.TestDispatchers
45import com.flipcash.app.persistence.sources.ChatMemberDataSource
56import com.flipcash.app.persistence.sources.ChatMessageDataSource
@@ -12,6 +13,7 @@ import com.flipcash.services.controllers.EventStreamingController
1213import com.flipcash.services.models.UserProfile
1314import com.flipcash.services.models.chat.ChatId
1415import com.flipcash.services.models.chat.ChatMember
16+ import com.flipcash.services.models.chat.ChatType
1517import com.flipcash.services.models.chat.ChatUpdate
1618import com.flipcash.services.models.chat.RosterChange
1719import com.flipcash.services.models.chat.RosterSummary
@@ -26,38 +28,45 @@ import com.flipcash.shared.chat.internal.delegates.GroupFeedDelegate
2628import com.flipcash.shared.chat.internal.delegates.MessagingDelegate
2729import com.getcode.utils.network.NetworkConnectivityListener
2830import com.getcode.opencode.model.accounts.AccountCluster
31+ import io.mockk.clearMocks
2932import io.mockk.coEvery
3033import io.mockk.coVerify
3134import io.mockk.every
3235import io.mockk.mockk
36+ import io.mockk.verify
3337import kotlinx.coroutines.ExperimentalCoroutinesApi
3438import kotlinx.coroutines.channels.Channel
3539import kotlinx.coroutines.flow.receiveAsFlow
3640import kotlinx.coroutines.test.TestCoroutineScheduler
41+ import kotlinx.coroutines.test.TestScope
3742import kotlinx.coroutines.test.runCurrent
3843import kotlinx.coroutines.test.runTest
3944import org.junit.Test
4045import org.junit.runner.RunWith
4146import org.robolectric.RobolectricTestRunner
4247
4348/* *
44- * The group work reaches the rest of the app through exactly two seams: a roster change on the
45- * event stream, and the login hook. Both are the coordinator's, so both are checked here rather
46- * than in the delegates that do the work.
49+ * The group work reaches the rest of the app through the coordinator and nowhere else: a roster
50+ * change on the event stream, and every trigger that re-syncs the conversation list. All of them
51+ * are checked here rather than in the delegates that do the work.
52+ *
53+ * The refresh triggers are the reason this file is not only about routing. A group's row comes
54+ * from `GroupFeedDelegate` alone, so a trigger that reaches `FeedSyncDelegate` by itself leaves
55+ * the group half of the list on whatever the login pass fetched — which is what these assert
56+ * against.
4757 */
4858@OptIn(ExperimentalCoroutinesApi ::class )
4959@RunWith(RobolectricTestRunner ::class )
5060class GroupChatRoutingTest {
5161
52- // Every test tears the coordinator down. The heartbeat the login hook starts is a `while
53- // (true)` on the test scheduler, so a test that leaves it running advances virtual time
54- // forever instead of finishing.
55-
5662 private val chatId = ChatId (" 11223344" )
5763 private val selfId = listOf<Byte >(1 , 2 , 3 )
5864 private val chatUpdatesChannel = Channel <ChatUpdate >(capacity = Channel .UNLIMITED )
5965
6066 private val groupFeedDelegate = mockk<GroupFeedDelegate >(relaxed = true )
67+ private val chatController = mockk<ChatController >(relaxed = true ).also {
68+ coEvery { it.getDmChatFeed(any(), any()) } returns Result .failure(RuntimeException (" not needed" ))
69+ }
6170
6271 private val testDispatchers = TestDispatchers (TestCoroutineScheduler ())
6372
@@ -70,9 +79,6 @@ class GroupChatRoutingTest {
7079 every { it.isConnected } returns true
7180 every { it.isStreamActive } returns true
7281 }
73- val chatController = mockk<ChatController >(relaxed = true ).also {
74- coEvery { it.getDmChatFeed(any(), any()) } returns Result .failure(RuntimeException (" not needed" ))
75- }
7682 val metadataDataSource = mockk<ChatMetadataDataSource >(relaxed = true )
7783 val messageDataSource = mockk<ChatMessageDataSource >(relaxed = true )
7884 val memberDataSource = mockk<ChatMemberDataSource >(relaxed = true )
@@ -135,40 +141,90 @@ class GroupChatRoutingTest {
135141 rosterSummary = RosterSummary (memberCount = 13 , version = 5 ),
136142 )
137143
138- @Test
139- fun `a roster change on the stream reaches the group delegate` () = runTest(testDispatchers.dispatcher) {
144+ /* *
145+ * Runs [block] against a logged-in coordinator and tears it down afterwards, however it ends.
146+ *
147+ * The `finally` is the point. The heartbeat the login hook starts is a `while (true)` on the
148+ * test scheduler, so a test that fails its assertion before reaching teardown hangs the run
149+ * advancing virtual time instead of reporting the failure.
150+ */
151+ private suspend fun TestScope.loggedIn (block : suspend (RealChatCoordinator ) -> Unit ) {
140152 val subject = coordinator()
141153 subject.onUserLoggedIn(mockk<AccountCluster >(relaxed = true ))
142154 runCurrent()
155+ try {
156+ block(subject)
157+ } finally {
158+ subject.teardown()
159+ }
160+ }
143161
144- chatUpdatesChannel.send(ChatUpdate (chatId = chatId, rosterUpdates = listOf (change)))
145- runCurrent()
162+ @Test
163+ fun `a roster change on the stream reaches the group delegate` () = runTest(testDispatchers.dispatcher) {
164+ loggedIn {
165+ chatUpdatesChannel.send(ChatUpdate (chatId = chatId, rosterUpdates = listOf (change)))
166+ runCurrent()
146167
147- coVerify { groupFeedDelegate.applyRosterChanges(chatId, listOf (change)) }
148- subject.teardown()
168+ coVerify { groupFeedDelegate.applyRosterChanges(chatId, listOf (change)) }
169+ }
149170 }
150171
151172 @Test
152173 fun `an update with no roster change does not reach the group delegate` () = runTest(testDispatchers.dispatcher) {
153- val subject = coordinator()
154- subject.onUserLoggedIn(mockk< AccountCluster >(relaxed = true ))
155- runCurrent()
174+ loggedIn {
175+ chatUpdatesChannel.send( ChatUpdate (chatId = chatId ))
176+ runCurrent()
156177
157- chatUpdatesChannel.send(ChatUpdate (chatId = chatId))
158- runCurrent()
159-
160- coVerify(exactly = 0 ) { groupFeedDelegate.applyRosterChanges(any(), any()) }
161- subject.teardown()
178+ coVerify(exactly = 0 ) { groupFeedDelegate.applyRosterChanges(any(), any()) }
179+ }
162180 }
163181
164182 @Test
165183 fun `logging in syncs the group feed` () = runTest(testDispatchers.dispatcher) {
166- val subject = coordinator()
167- subject.onUserLoggedIn(mockk<AccountCluster >(relaxed = true ))
168- runCurrent()
184+ loggedIn {
185+ verify(exactly = 1 ) { groupFeedDelegate.syncGroupFeed() }
186+ }
187+ }
169188
170- coVerify { groupFeedDelegate.syncGroupFeed() }
171- subject.teardown()
189+ @Test
190+ fun `refreshing the feed syncs the group feed` () = runTest(testDispatchers.dispatcher) {
191+ loggedIn { subject ->
192+ clearMocks(groupFeedDelegate, answers = false )
193+
194+ // What a chat push and both payment delegates call.
195+ subject.refreshFeed()
196+ runCurrent()
197+
198+ verify(exactly = 1 ) { groupFeedDelegate.syncGroupFeed() }
199+ }
200+ }
201+
202+ @Test
203+ fun `refreshing the feed still syncs the DM feeds` () = runTest(testDispatchers.dispatcher) {
204+ loggedIn { subject ->
205+ clearMocks(chatController, answers = false )
206+
207+ // [RealChatCoordinator.refreshFeed] overrides the FeedOperations delegation, so the
208+ // half that used to be the only one running needs asserting as well as the half that
209+ // did not.
210+ subject.refreshFeed()
211+ runCurrent()
212+
213+ coVerify(exactly = 1 ) { chatController.getDmChatFeed(ChatType .CONTACT_DM , any()) }
214+ coVerify(exactly = 1 ) { chatController.getDmChatFeed(ChatType .TIP_DM , any()) }
215+ }
216+ }
217+
218+ @Test
219+ fun `resuming from the background syncs the group feed` () = runTest(testDispatchers.dispatcher) {
220+ loggedIn { subject ->
221+ clearMocks(groupFeedDelegate, answers = false )
222+
223+ subject.onStart(mockk<LifecycleOwner >(relaxed = true ))
224+ runCurrent()
225+
226+ verify(exactly = 1 ) { groupFeedDelegate.syncGroupFeed() }
227+ }
172228 }
173229
174230 @Test
0 commit comments