Skip to content

Commit 14b4fe3

Browse files
committed
test(messaging): add tests for new ChatMessagingController RPCs
Cover editMessage, deleteMessage, addReaction, removeReaction, getReactors, getReactionSummary, getReactionSummaries, and getDelta with no-account-cluster, parameter-forwarding, and result-surfacing tests. Wire up FakeChatMessagingRepository with tracked state for all new repository methods.
1 parent 43dbecd commit 14b4fe3

1 file changed

Lines changed: 341 additions & 0 deletions

File tree

services/flipcash/src/test/kotlin/com/flipcash/services/controllers/ChatMessagingControllerTest.kt

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@ import com.flipcash.services.models.QueryOptions
44
import com.flipcash.services.models.chat.ChatId
55
import com.flipcash.services.models.chat.ChatMessage
66
import com.flipcash.services.models.chat.ClientMessageId
7+
import com.flipcash.services.models.chat.Emoji
8+
import com.flipcash.services.models.chat.EmojiReaction
79
import com.flipcash.services.models.chat.MessageContent
810
import com.flipcash.services.models.chat.PointerType
11+
import com.flipcash.services.models.chat.Reactor
12+
import com.flipcash.services.models.chat.ReactionSummary
913
import com.flipcash.services.models.chat.TypingState
1014
import 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
1119
import com.flipcash.services.user.UserManager
1220
import com.getcode.ed25519.Ed25519
1321
import 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

Comments
 (0)