messenger-android

Android graphical user interfaces for GNUnet Messenger
Log | Files | Refs | README | LICENSE

commit 51e33eccd6f0ef4bb8d87140e5755925369d4296
parent 6db3d671e3acee5b553bf7eb62f67dcae3ef52e9
Author: t3sserakt <t3ss@posteo.de>
Date:   Tue, 12 May 2026 16:03:33 +0200

WIP: one to one test

Diffstat:
AGNUnetMessenger/app/src/androidTest/java/org/gnunet/gnunetmessenger/ipc/Gnunet1to1MessagingTest.kt | 250+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 250 insertions(+), 0 deletions(-)

diff --git a/GNUnetMessenger/app/src/androidTest/java/org/gnunet/gnunetmessenger/ipc/Gnunet1to1MessagingTest.kt b/GNUnetMessenger/app/src/androidTest/java/org/gnunet/gnunetmessenger/ipc/Gnunet1to1MessagingTest.kt @@ -0,0 +1,250 @@ +package org.gnunet.gnunetmessenger.ipc + +import android.util.Log +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import org.gnunet.gnunetmessenger.model.ChatAccount +import org.gnunet.gnunetmessenger.model.ChatContact +import org.gnunet.gnunetmessenger.model.ChatContext +import org.gnunet.gnunetmessenger.model.ChatHandle +import org.gnunet.gnunetmessenger.model.ChatMessage +import org.gnunet.gnunetmessenger.model.MessageKind +import org.gnunet.gnunetmessenger.model.MessengerApp +import org.gnunet.gnunetmessenger.service.boundimpl.GnunetChatBoundService +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +/** + * End-to-end 1:1 messaging test. Pairs two accounts via lobby, then verifies + * bidirectional text delivery: A -> B, then B -> A. Each direction asserts + * the receiver sees a TEXT message whose body matches the sent text and whose + * sender name matches the sending account. + * + * This is the regression test for the send-text bug that was fixed via the + * separate `nativeContextPointer` field on ChatContext. If the IPC pointer + * plumbing breaks again, sendText silently no-ops and the receiver wait + * times out. + */ +@RunWith(AndroidJUnit4::class) +class Gnunet1to1MessagingTest { + + private val tag = "OneToOneMsg" + private val appContext = ApplicationProvider.getApplicationContext<android.content.Context>() + private val svcA = GnunetChatBoundService(appContext) + private val svcB = GnunetChatBoundService(appContext) + private val logA = mutableListOf<Pair<ChatContext, ChatMessage>>() + private val logB = mutableListOf<Pair<ChatContext, ChatMessage>>() + + // libgnunetchat stores account names lower-cased; keep them lower-case + // so receiver-side sender.name comparison matches. + private val ts = System.currentTimeMillis() + private val nameA = "msgtesta-$ts" + private val nameB = "msgtestb-$ts" + + @After + fun tearDown() = runTest { + runCatching { svcA.unbind() } + runCatching { svcB.unbind() } + delay(1000) + } + + private suspend fun waitForHandle(label: String, handle: ChatHandle, timeoutMs: Long = 30_000) { + withContext(Dispatchers.Default.limitedParallelism(1)) { + withTimeout(timeoutMs) { + while (handle.pointer == 0L) delay(100) + } + } + Log.i(tag, "$label: handle ready -> ${handle.pointer}") + } + + private suspend fun waitForRefresh( + label: String, + log: List<Pair<ChatContext, ChatMessage>>, + timeoutMs: Long = 30_000 + ) { + withContext(Dispatchers.Default.limitedParallelism(1)) { + withTimeout(timeoutMs) { + while (log.none { it.second.kind == MessageKind.REFRESH }) delay(100) + } + } + Log.i(tag, "$label: REFRESH received") + } + + private suspend fun connectAccount( + label: String, + svc: GnunetChatBoundService, + handle: ChatHandle, + name: String, + log: List<Pair<ChatContext, ChatMessage>> + ): ChatAccount { + Log.i(tag, "$label: createAccount('$name')") + svc.createAccount(handle, name) + + var found: ChatAccount? = null + withContext(Dispatchers.Default.limitedParallelism(1)) { + withTimeout(45_000) { + while (found == null) { + val snapshot = mutableListOf<ChatAccount>() + svc.iterateAccounts(handle) { snapshot += it } + delay(400) + found = snapshot.firstOrNull { it.name.equals(name, ignoreCase = true) } + if (found == null) delay(500) + } + } + } + val account = found!! + Log.i(tag, "$label: account '${account.name}' visible; calling connect") + svc.connect(handle, account) + + withContext(Dispatchers.Default.limitedParallelism(1)) { + withTimeout(20_000) { + while (log.none { it.second.kind == MessageKind.LOGIN }) delay(100) + } + } + Log.i(tag, "$label: LOGIN received") + return account + } + + /** + * Wait until [receiverLog] contains a TEXT message whose body equals + * [expectedText] AND whose sender name equals [expectedSenderName]. + * Returns the matching ChatMessage. Filters out the sender's own echo + * (which would appear with sender.name == self). + */ + private suspend fun waitForIncomingText( + receiverLabel: String, + receiverLog: List<Pair<ChatContext, ChatMessage>>, + expectedText: String, + expectedSenderName: String, + timeoutMs: Long = 60_000 + ): ChatMessage { + var match: ChatMessage? = null + withContext(Dispatchers.Default.limitedParallelism(1)) { + withTimeout(timeoutMs) { + while (match == null) { + match = receiverLog + .map { it.second } + .firstOrNull { msg -> + msg.kind == MessageKind.TEXT && + msg.text == expectedText && + msg.sender?.name?.equals(expectedSenderName, ignoreCase = true) == true + } + if (match == null) delay(200) + } + } + } + Log.i( + tag, + "$receiverLabel: incoming TEXT received from '$expectedSenderName' body='$expectedText'" + ) + return match!! + } + + @Test + fun bidirectionalTextMessageDeliversBetweenLobbyPairedAccounts() = runTest { + Log.i(tag, "=== START: bidirectionalTextMessageDeliversBetweenLobbyPairedAccounts ===") + + // 1. Bring up two parallel chat handles. + val handleA = svcA.startChat(MessengerApp()) { ctx, msg -> + logA += ctx to msg + Log.d(tag, "A onMessage: kind=${msg.kind} sender=${msg.sender?.name} text='${msg.text}'") + } + val handleB = svcB.startChat(MessengerApp()) { ctx, msg -> + logB += ctx to msg + Log.d(tag, "B onMessage: kind=${msg.kind} sender=${msg.sender?.name} text='${msg.text}'") + } + waitForHandle("A", handleA) + waitForHandle("B", handleB) + waitForRefresh("A", logA) + waitForRefresh("B", logB) + + // 2. Create and connect an account on each handle. + connectAccount("A", svcA, handleA, nameA, logA) + connectAccount("B", svcB, handleB, nameB, logB) + + // 3. Pair them via lobby. + Log.i(tag, "A: lobbyOpen") + var lobbyUri = "" + svcA.lobbyOpen(handleA) { uri -> lobbyUri = uri } + withContext(Dispatchers.Default.limitedParallelism(1)) { + withTimeout(30_000) { while (lobbyUri.isEmpty()) delay(200) } + } + assertTrue("Lobby URI must be delivered", lobbyUri.isNotEmpty()) + + Log.i(tag, "B: lobbyJoin") + svcB.lobbyJoin(handleB, lobbyUri) + + // 4. Wait for each side to see the other as a contact. + Log.i(tag, "Waiting for contact convergence") + var contactsA: List<ChatContact> = emptyList() + var contactsB: List<ChatContact> = emptyList() + withContext(Dispatchers.Default.limitedParallelism(1)) { + withTimeout(60_000) { + while (true) { + contactsA = svcA.listContacts(handleA) + contactsB = svcB.listContacts(handleB) + val aHasB = contactsA.any { it.name.equals(nameB, ignoreCase = true) } + val bHasA = contactsB.any { it.name.equals(nameA, ignoreCase = true) } + if (aHasB && bHasA) break + delay(500) + } + } + } + Log.i(tag, "A contacts: ${contactsA.map { it.name }}") + Log.i(tag, "B contacts: ${contactsB.map { it.name }}") + + // 5. Resolve the 1:1 contexts on each side. + val contactBfromA = contactsA.first { it.name.equals(nameB, ignoreCase = true) } + val contactAfromB = contactsB.first { it.name.equals(nameA, ignoreCase = true) } + val ctxAtoB = svcA.getContactContext(contactBfromA) + val ctxBtoA = svcB.getContactContext(contactAfromB) + Log.i( + tag, + "Contexts: A->B nativePtr=${ctxAtoB.nativeContextPointer} B->A nativePtr=${ctxBtoA.nativeContextPointer}" + ) + + // 6. Send A -> B and assert receipt on B. + val bodyAtoB = "ping-from-A-$ts-${(1000..9999).random()}" + Log.i(tag, "A.sendText -> '$bodyAtoB'") + svcA.sendText(ctxAtoB, bodyAtoB) + val receivedOnB = waitForIncomingText( + receiverLabel = "B", + receiverLog = logB, + expectedText = bodyAtoB, + expectedSenderName = nameA + ) + assertEquals("Body received on B must equal body sent by A", bodyAtoB, receivedOnB.text) + assertEquals( + "Sender on B's received message must be A", + nameA.lowercase(), + receivedOnB.sender?.name?.lowercase() + ) + + // 7. Send B -> A and assert receipt on A. + val bodyBtoA = "pong-from-B-$ts-${(1000..9999).random()}" + Log.i(tag, "B.sendText -> '$bodyBtoA'") + svcB.sendText(ctxBtoA, bodyBtoA) + val receivedOnA = waitForIncomingText( + receiverLabel = "A", + receiverLog = logA, + expectedText = bodyBtoA, + expectedSenderName = nameB + ) + assertEquals("Body received on A must equal body sent by B", bodyBtoA, receivedOnA.text) + assertEquals( + "Sender on A's received message must be B", + nameB.lowercase(), + receivedOnA.sender?.name?.lowercase() + ) + + Log.i(tag, "=== PASS: bidirectional 1:1 text delivery verified ===") + } +}