@@ -4,10 +4,18 @@ import com.flipcash.services.models.QueryOptions
44import com.flipcash.services.models.chat.ChatId
55import com.flipcash.services.models.chat.ChatMessage
66import com.flipcash.services.models.chat.ClientMessageId
7+ import com.flipcash.services.models.chat.Emoji
8+ import com.flipcash.services.models.chat.EmojiReaction
79import com.flipcash.services.models.chat.MessageContent
810import com.flipcash.services.models.chat.PointerType
11+ import com.flipcash.services.models.chat.Reactor
12+ import com.flipcash.services.models.chat.ReactionSummary
913import com.flipcash.services.models.chat.TypingState
1014import com.flipcash.services.repository.ChatMessagingRepository
15+ import com.flipcash.services.repository.DeltaUpdate
16+ import com.flipcash.services.repository.ReactorsPage
17+ import kotlinx.coroutines.flow.Flow
18+ import kotlinx.coroutines.flow.flowOf
1119import com.flipcash.services.user.UserManager
1220import com.getcode.ed25519.Ed25519
1321import com.getcode.opencode.model.accounts.AccountCluster
@@ -47,6 +55,14 @@ class ChatMessagingControllerTest {
4755 unreadSeq = id,
4856 )
4957
58+ private fun stubReaction (emoji : Emoji , count : Long = 1) = EmojiReaction (
59+ emoji = emoji,
60+ count = count,
61+ reactedBySelf = false ,
62+ sampleReactors = emptyList(),
63+ sequence = 1 ,
64+ )
65+
5066 // region getMessage
5167
5268 @Test
@@ -183,6 +199,281 @@ class ChatMessagingControllerTest {
183199 }
184200
185201 // endregion
202+
203+ // region editMessage
204+
205+ @Test
206+ fun `editMessage fails when no account cluster` () = runTest {
207+ every { userManager.accountCluster } returns null
208+ val result = controller.editMessage(testChatId, 1 , listOf (MessageContent .Text (" edited" )), 5 )
209+ assertTrue(result.isFailure)
210+ assertIs<IllegalStateException >(result.exceptionOrNull())
211+ }
212+
213+ @Test
214+ fun `editMessage forwards parameters` () = runTest {
215+ stubOwner()
216+ val content = listOf (MessageContent .Text (" edited" ))
217+ repository.editMessageResult = Result .success(stubMessage(1 , " edited" ))
218+
219+ controller.editMessage(testChatId, 1 , content, 5 )
220+
221+ assertEquals(testChatId, repository.lastChatId)
222+ assertEquals(1L , repository.lastMessageId)
223+ assertEquals(content, repository.lastContent)
224+ assertEquals(5L , repository.lastExpectedEventSequence)
225+ }
226+
227+ @Test
228+ fun `editMessage returns updated message` () = runTest {
229+ stubOwner()
230+ val edited = stubMessage(1 , " edited" )
231+ repository.editMessageResult = Result .success(edited)
232+
233+ val result = controller.editMessage(testChatId, 1 , listOf (MessageContent .Text (" edited" )), 5 )
234+
235+ assertEquals(" edited" , (result.getOrThrow().content.first() as MessageContent .Text ).text)
236+ }
237+
238+ // endregion
239+
240+ // region deleteMessage
241+
242+ @Test
243+ fun `deleteMessage fails when no account cluster` () = runTest {
244+ every { userManager.accountCluster } returns null
245+ val result = controller.deleteMessage(testChatId, 1 , 5 )
246+ assertTrue(result.isFailure)
247+ assertIs<IllegalStateException >(result.exceptionOrNull())
248+ }
249+
250+ @Test
251+ fun `deleteMessage forwards parameters` () = runTest {
252+ stubOwner()
253+ repository.deleteMessageResult = Result .success(stubMessage(1 ))
254+
255+ controller.deleteMessage(testChatId, 1 , 5 )
256+
257+ assertEquals(testChatId, repository.lastChatId)
258+ assertEquals(1L , repository.lastMessageId)
259+ assertEquals(5L , repository.lastExpectedEventSequence)
260+ }
261+
262+ @Test
263+ fun `deleteMessage surfaces repository failure` () = runTest {
264+ stubOwner()
265+ val cause = RuntimeException (" not found" )
266+ repository.deleteMessageResult = Result .failure(cause)
267+
268+ val result = controller.deleteMessage(testChatId, 1 , 5 )
269+
270+ assertTrue(result.isFailure)
271+ assertSame(cause, result.exceptionOrNull())
272+ }
273+
274+ // endregion
275+
276+ // region addReaction
277+
278+ @Test
279+ fun `addReaction fails when no account cluster` () = runTest {
280+ every { userManager.accountCluster } returns null
281+ val result = controller.addReaction(testChatId, 1 , Emoji (" \uD83D\uDC4D " ))
282+ assertTrue(result.isFailure)
283+ assertIs<IllegalStateException >(result.exceptionOrNull())
284+ }
285+
286+ @Test
287+ fun `addReaction forwards parameters` () = runTest {
288+ stubOwner()
289+ val emoji = Emoji (" \uD83D\uDC4D " )
290+ repository.addReactionResult = Result .success(stubReaction(emoji))
291+
292+ controller.addReaction(testChatId, 1 , emoji)
293+
294+ assertEquals(testChatId, repository.lastChatId)
295+ assertEquals(1L , repository.lastMessageId)
296+ assertEquals(emoji, repository.lastEmoji)
297+ }
298+
299+ @Test
300+ fun `addReaction returns reaction from repository` () = runTest {
301+ stubOwner()
302+ val emoji = Emoji (" \uD83D\uDC4D " )
303+ val reaction = stubReaction(emoji, count = 3 )
304+ repository.addReactionResult = Result .success(reaction)
305+
306+ val result = controller.addReaction(testChatId, 1 , emoji)
307+
308+ assertEquals(3L , result.getOrThrow().count)
309+ assertEquals(emoji, result.getOrThrow().emoji)
310+ }
311+
312+ // endregion
313+
314+ // region removeReaction
315+
316+ @Test
317+ fun `removeReaction fails when no account cluster` () = runTest {
318+ every { userManager.accountCluster } returns null
319+ val result = controller.removeReaction(testChatId, 1 , Emoji (" \uD83D\uDC4D " ))
320+ assertTrue(result.isFailure)
321+ assertIs<IllegalStateException >(result.exceptionOrNull())
322+ }
323+
324+ @Test
325+ fun `removeReaction forwards parameters` () = runTest {
326+ stubOwner()
327+ val emoji = Emoji (" \uD83D\uDE00 " )
328+ repository.removeReactionResult = Result .success(stubReaction(emoji, count = 0 ))
329+
330+ controller.removeReaction(testChatId, 1 , emoji)
331+
332+ assertEquals(testChatId, repository.lastChatId)
333+ assertEquals(1L , repository.lastMessageId)
334+ assertEquals(emoji, repository.lastEmoji)
335+ }
336+
337+ // endregion
338+
339+ // region getReactors
340+
341+ @Test
342+ fun `getReactors fails when no account cluster` () = runTest {
343+ every { userManager.accountCluster } returns null
344+ val result = controller.getReactors(testChatId, 1 , Emoji (" \uD83D\uDC4D " ))
345+ assertTrue(result.isFailure)
346+ assertIs<IllegalStateException >(result.exceptionOrNull())
347+ }
348+
349+ @Test
350+ fun `getReactors forwards parameters with default QueryOptions` () = runTest {
351+ stubOwner()
352+ val emoji = Emoji (" \uD83D\uDC4D " )
353+ repository.getReactorsResult = Result .success(ReactorsPage (emptyList(), hasMore = false ))
354+
355+ controller.getReactors(testChatId, 1 , emoji)
356+
357+ assertEquals(testChatId, repository.lastChatId)
358+ assertEquals(1L , repository.lastMessageId)
359+ assertEquals(emoji, repository.lastEmoji)
360+ assertEquals(QueryOptions (), repository.lastQueryOptions)
361+ }
362+
363+ @Test
364+ fun `getReactors returns page from repository` () = runTest {
365+ stubOwner()
366+ val emoji = Emoji (" \uD83D\uDC4D " )
367+ val page = ReactorsPage (
368+ reactors = listOf (Reactor (userId = listOf (1 .toByte()), reactedAt = Instant .fromEpochSeconds(1000 ))),
369+ hasMore = true ,
370+ )
371+ repository.getReactorsResult = Result .success(page)
372+
373+ val result = controller.getReactors(testChatId, 1 , emoji)
374+
375+ assertEquals(1 , result.getOrThrow().reactors.size)
376+ assertTrue(result.getOrThrow().hasMore)
377+ }
378+
379+ // endregion
380+
381+ // region getReactionSummary
382+
383+ @Test
384+ fun `getReactionSummary fails when no account cluster` () = runTest {
385+ every { userManager.accountCluster } returns null
386+ val result = controller.getReactionSummary(testChatId, 1 )
387+ assertTrue(result.isFailure)
388+ assertIs<IllegalStateException >(result.exceptionOrNull())
389+ }
390+
391+ @Test
392+ fun `getReactionSummary forwards chatId and messageId` () = runTest {
393+ stubOwner()
394+ repository.getReactionSummaryResult = Result .success(ReactionSummary (messageId = 42 , reactions = emptyList()))
395+
396+ controller.getReactionSummary(testChatId, 42 )
397+
398+ assertEquals(testChatId, repository.lastChatId)
399+ assertEquals(42L , repository.lastMessageId)
400+ }
401+
402+ @Test
403+ fun `getReactionSummary returns summary from repository` () = runTest {
404+ stubOwner()
405+ val summary = ReactionSummary (
406+ messageId = 1 ,
407+ reactions = listOf (stubReaction(Emoji (" \uD83D\uDC4D " ), count = 5 )),
408+ )
409+ repository.getReactionSummaryResult = Result .success(summary)
410+
411+ val result = controller.getReactionSummary(testChatId, 1 )
412+
413+ assertEquals(1 , result.getOrThrow().reactions.size)
414+ assertEquals(5L , result.getOrThrow().reactions.first().count)
415+ }
416+
417+ // endregion
418+
419+ // region getReactionSummaries
420+
421+ @Test
422+ fun `getReactionSummaries fails when no account cluster` () = runTest {
423+ every { userManager.accountCluster } returns null
424+ val result = controller.getReactionSummaries(testChatId)
425+ assertTrue(result.isFailure)
426+ assertIs<IllegalStateException >(result.exceptionOrNull())
427+ }
428+
429+ @Test
430+ fun `getReactionSummaries uses default QueryOptions` () = runTest {
431+ stubOwner()
432+ repository.getReactionSummariesResult = Result .success(emptyList())
433+
434+ controller.getReactionSummaries(testChatId)
435+
436+ assertEquals(testChatId, repository.lastChatId)
437+ assertEquals(QueryOptions (), repository.lastQueryOptions)
438+ }
439+
440+ @Test
441+ fun `getReactionSummaries returns list from repository` () = runTest {
442+ stubOwner()
443+ val summaries = listOf (
444+ ReactionSummary (messageId = 1 , reactions = listOf (stubReaction(Emoji (" \uD83D\uDC4D " )))),
445+ ReactionSummary (messageId = 2 , reactions = emptyList()),
446+ )
447+ repository.getReactionSummariesResult = Result .success(summaries)
448+
449+ val result = controller.getReactionSummaries(testChatId)
450+
451+ assertEquals(2 , result.getOrThrow().size)
452+ }
453+
454+ // endregion
455+
456+ // region getDelta
457+
458+ @Test
459+ fun `getDelta fails when no account cluster` () = runTest {
460+ every { userManager.accountCluster } returns null
461+ val result = runCatching { controller.getDelta(testChatId, 0 ) }
462+ assertTrue(result.isFailure)
463+ assertIs<IllegalStateException >(result.exceptionOrNull())
464+ }
465+
466+ @Test
467+ fun `getDelta forwards chatId and afterSequence` () = runTest {
468+ stubOwner()
469+
470+ controller.getDelta(testChatId, 10 )
471+
472+ assertEquals(testChatId, repository.lastChatId)
473+ assertEquals(10L , repository.lastAfterSequence)
474+ }
475+
476+ // endregion
186477}
187478
188479// region Fakes
@@ -192,15 +483,25 @@ private class FakeChatMessagingRepository : ChatMessagingRepository {
192483 var getMessagesResult: Result <List <ChatMessage >> = Result .failure(RuntimeException (" not configured" ))
193484 var getMessagesByIdsResult: Result <List <ChatMessage >> = Result .failure(RuntimeException (" not configured" ))
194485 var sendMessageResult: Result <ChatMessage > = Result .failure(RuntimeException (" not configured" ))
486+ var editMessageResult: Result <ChatMessage > = Result .failure(RuntimeException (" not configured" ))
487+ var deleteMessageResult: Result <ChatMessage > = Result .failure(RuntimeException (" not configured" ))
488+ var addReactionResult: Result <EmojiReaction > = Result .failure(RuntimeException (" not configured" ))
489+ var removeReactionResult: Result <EmojiReaction > = Result .failure(RuntimeException (" not configured" ))
490+ var getReactorsResult: Result <ReactorsPage > = Result .failure(RuntimeException (" not configured" ))
491+ var getReactionSummaryResult: Result <ReactionSummary > = Result .failure(RuntimeException (" not configured" ))
492+ var getReactionSummariesResult: Result <List <ReactionSummary >> = Result .failure(RuntimeException (" not configured" ))
195493 var advancePointerResult: Result <Unit > = Result .failure(RuntimeException (" not configured" ))
196494 var notifyIsTypingResult: Result <Unit > = Result .failure(RuntimeException (" not configured" ))
197495
198496 var lastChatId: ChatId ? = null
199497 var lastMessageId: Long? = null
200498 var lastMessageIds: List <Long >? = null
499+ var lastAfterSequence: Long? = null
201500 var lastQueryOptions: QueryOptions ? = null
202501 var lastContent: List <MessageContent >? = null
203502 var lastClientMessageId: ClientMessageId ? = null
503+ var lastExpectedEventSequence: Long? = null
504+ var lastEmoji: Emoji ? = null
204505 var lastPointerType: PointerType ? = null
205506 var lastTypingState: TypingState ? = null
206507
@@ -219,11 +520,51 @@ private class FakeChatMessagingRepository : ChatMessagingRepository {
219520 return getMessagesByIdsResult
220521 }
221522
523+ override fun getDelta (owner : Ed25519 .KeyPair , chatId : ChatId , afterSequence : Long ): Flow <Result <DeltaUpdate >> {
524+ lastChatId = chatId; lastAfterSequence = afterSequence
525+ return flowOf(Result .failure(RuntimeException (" not configured" )))
526+ }
527+
222528 override suspend fun sendMessage (owner : Ed25519 .KeyPair , chatId : ChatId , content : List <MessageContent >, clientMessageId : ClientMessageId ): Result <ChatMessage > {
223529 lastChatId = chatId; lastContent = content; lastClientMessageId = clientMessageId
224530 return sendMessageResult
225531 }
226532
533+ override suspend fun editMessage (owner : Ed25519 .KeyPair , chatId : ChatId , messageId : Long , content : List <MessageContent >, expectedEventSequence : Long ): Result <ChatMessage > {
534+ lastChatId = chatId; lastMessageId = messageId; lastContent = content; lastExpectedEventSequence = expectedEventSequence
535+ return editMessageResult
536+ }
537+
538+ override suspend fun deleteMessage (owner : Ed25519 .KeyPair , chatId : ChatId , messageId : Long , expectedEventSequence : Long ): Result <ChatMessage > {
539+ lastChatId = chatId; lastMessageId = messageId; lastExpectedEventSequence = expectedEventSequence
540+ return deleteMessageResult
541+ }
542+
543+ override suspend fun addReaction (owner : Ed25519 .KeyPair , chatId : ChatId , messageId : Long , emoji : Emoji ): Result <EmojiReaction > {
544+ lastChatId = chatId; lastMessageId = messageId; lastEmoji = emoji
545+ return addReactionResult
546+ }
547+
548+ override suspend fun removeReaction (owner : Ed25519 .KeyPair , chatId : ChatId , messageId : Long , emoji : Emoji ): Result <EmojiReaction > {
549+ lastChatId = chatId; lastMessageId = messageId; lastEmoji = emoji
550+ return removeReactionResult
551+ }
552+
553+ override suspend fun getReactors (owner : Ed25519 .KeyPair , chatId : ChatId , messageId : Long , emoji : Emoji , queryOptions : QueryOptions ): Result <ReactorsPage > {
554+ lastChatId = chatId; lastMessageId = messageId; lastEmoji = emoji; lastQueryOptions = queryOptions
555+ return getReactorsResult
556+ }
557+
558+ override suspend fun getReactionSummary (owner : Ed25519 .KeyPair , chatId : ChatId , messageId : Long ): Result <ReactionSummary > {
559+ lastChatId = chatId; lastMessageId = messageId
560+ return getReactionSummaryResult
561+ }
562+
563+ override suspend fun getReactionSummaries (owner : Ed25519 .KeyPair , chatId : ChatId , queryOptions : QueryOptions ): Result <List <ReactionSummary >> {
564+ lastChatId = chatId; lastQueryOptions = queryOptions
565+ return getReactionSummariesResult
566+ }
567+
227568 override suspend fun advancePointer (owner : Ed25519 .KeyPair , chatId : ChatId , pointerType : PointerType , messageId : Long ): Result <Unit > {
228569 lastChatId = chatId; lastPointerType = pointerType; lastMessageId = messageId
229570 return advancePointerResult
0 commit comments