messenger-android

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

commit 2945805313541f21949712cfe49f17c6650fdef0
parent f9b609a1cbad55c071894f755eb228a92851089b
Author: t3sserakt <t3ss@posteo.de>
Date:   Mon, 25 May 2026 20:45:56 +0200

one to one message fixed

Diffstat:
MGNUnetMessenger/app/src/androidTest/java/org/gnunet/gnunetmessenger/ipc/Gnunet1to1MessagingTest.kt | 192++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
1 file changed, 156 insertions(+), 36 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 @@ -5,7 +5,7 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import org.gnunet.gnunetmessenger.model.ChatAccount @@ -18,8 +18,12 @@ 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.assertNotEquals +import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.Test +import kotlin.time.Duration.Companion.minutes import org.junit.runner.RunWith /** @@ -50,13 +54,16 @@ class Gnunet1to1MessagingTest { private val nameB = "msgtestb-$ts" @After - fun tearDown() = runBlocking { + fun tearDown() = runTest { runCatching { svcA.unbind() } runCatching { svcB.unbind() } - delay(1000) + delay(1_000) } + // ── Helpers ──────────────────────────────────────────────────────── + private suspend fun waitForHandle(label: String, handle: ChatHandle, timeoutMs: Long = 30_000) { + Log.i(tag, "$label: waitForHandle (timeout=${timeoutMs}ms)") withContext(Dispatchers.Default.limitedParallelism(1)) { withTimeout(timeoutMs) { while (handle.pointer == 0L) delay(100) @@ -70,6 +77,7 @@ class Gnunet1to1MessagingTest { log: List<Pair<ChatContext, ChatMessage>>, timeoutMs: Long = 30_000 ) { + Log.i(tag, "$label: waitForRefresh") withContext(Dispatchers.Default.limitedParallelism(1)) { withTimeout(timeoutMs) { while (log.none { it.second.kind == MessageKind.REFRESH }) delay(100) @@ -78,6 +86,21 @@ class Gnunet1to1MessagingTest { Log.i(tag, "$label: REFRESH received") } + /** + * Takes a snapshot of visible accounts via the fire-and-forget + * [GnunetChatBoundService.iterateAccounts] call, with a small drain + * delay so the binder callback can deliver results. + */ + private suspend fun snapshotAccounts( + svc: GnunetChatBoundService, + handle: ChatHandle + ): List<ChatAccount> { + val acc = mutableListOf<ChatAccount>() + svc.iterateAccounts(handle) { acc += it } + delay(400) + return acc.toList() + } + private suspend fun connectAccount( label: String, svc: GnunetChatBoundService, @@ -86,15 +109,16 @@ class Gnunet1to1MessagingTest { log: List<Pair<ChatContext, ChatMessage>> ): ChatAccount { Log.i(tag, "$label: createAccount('$name')") - svc.createAccount(handle, name) + val rc = svc.createAccount(handle, name) + Log.i(tag, "$label: createAccount returned $rc") + Log.i(tag, "$label: polling iterateAccounts until '$name' appears") 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) + val snapshot = snapshotAccounts(svc, handle) + Log.d(tag, "$label: snapshot=${snapshot.map { it.name }}") found = snapshot.firstOrNull { it.name.equals(name, ignoreCase = true) } if (found == null) delay(500) } @@ -102,8 +126,11 @@ class Gnunet1to1MessagingTest { } val account = found!! Log.i(tag, "$label: account '${account.name}' visible; calling connect") + + Log.i(tag, "$label: connect('$name')") svc.connect(handle, account) + Log.i(tag, "$label: waiting for LOGIN") withContext(Dispatchers.Default.limitedParallelism(1)) { withTimeout(20_000) { while (log.none { it.second.kind == MessageKind.LOGIN }) delay(100) @@ -116,8 +143,7 @@ class Gnunet1to1MessagingTest { /** * 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). + * Returns the matching ChatMessage. */ private suspend fun waitForIncomingText( receiverLabel: String, @@ -126,6 +152,7 @@ class Gnunet1to1MessagingTest { expectedSenderName: String, timeoutMs: Long = 60_000 ): ChatMessage { + Log.i(tag, "$receiverLabel: waiting for TEXT from '$expectedSenderName' body='$expectedText' (timeout=${timeoutMs}ms)") var match: ChatMessage? = null withContext(Dispatchers.Default.limitedParallelism(1)) { withTimeout(timeoutMs) { @@ -141,49 +168,72 @@ class Gnunet1to1MessagingTest { } } } - Log.i( - tag, - "$receiverLabel: incoming TEXT received from '$expectedSenderName' body='$expectedText'" - ) + Log.i(tag, "$receiverLabel: received TEXT from '$expectedSenderName' body='$expectedText'") return match!! } + // ── Test ─────────────────────────────────────────────────────────── + @Test - fun bidirectionalTextMessageDeliversBetweenLobbyPairedAccounts() = runBlocking<Unit> { + fun bidirectionalTextMessageDeliversBetweenLobbyPairedAccounts() = runTest(timeout = 5.minutes) { + // 5-minute wall-clock timeout so GNUnet DHT routing has time to + // complete. runTest's default 60 s is too short for first-time + // P2P message delivery on a single device. Log.i(tag, "=== START: bidirectionalTextMessageDeliversBetweenLobbyPairedAccounts ===") - // 1. Bring up two parallel chat handles. + // ── Step 1: Bring up two parallel chat handles ────────────── + Log.i(tag, "Step 1: Starting 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}'") + Log.d(tag, "A onMessage: kind=${msg.kind} sender=${msg.sender?.name} text='${msg.text?.take(40)}'") } 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}'") + Log.d(tag, "B onMessage: kind=${msg.kind} sender=${msg.sender?.name} text='${msg.text?.take(40)}'") } + waitForHandle("A", handleA) waitForHandle("B", handleB) + + assertTrue("Handle A must be non-zero", handleA.pointer != 0L) + assertTrue("Handle B must be non-zero", handleB.pointer != 0L) + assertNotEquals( + "Two startChat calls must produce distinct sessions", + handleA.pointer, + handleB.pointer + ) + Log.i(tag, "Step 1 OK: handleA=${handleA.pointer} handleB=${handleB.pointer}") + waitForRefresh("A", logA) waitForRefresh("B", logB) - // 2. Create and connect an account on each handle. + // ── Step 2: Create and connect accounts ───────────────────── + Log.i(tag, "Step 2: Creating and connecting accounts") connectAccount("A", svcA, handleA, nameA, logA) connectAccount("B", svcB, handleB, nameB, logB) + Log.i(tag, "Step 2 OK: both accounts connected") - // 3. Pair them via lobby. + // ── Step 3: Pair via lobby ────────────────────────────────── + Log.i(tag, "Step 3: Lobby pairing") Log.i(tag, "A: lobbyOpen") var lobbyUri = "" - svcA.lobbyOpen(handleA) { uri -> lobbyUri = uri } + svcA.lobbyOpen(handleA) { uri -> + Log.i(tag, "A: onLobbyUri received (${uri.length} chars)") + lobbyUri = uri + } withContext(Dispatchers.Default.limitedParallelism(1)) { - withTimeout(30_000) { while (lobbyUri.isEmpty()) delay(200) } + withTimeout(30_000) { + while (lobbyUri.isEmpty()) delay(200) + } } assertTrue("Lobby URI must be delivered", lobbyUri.isNotEmpty()) + Log.i(tag, "Lobby URI: ${lobbyUri.take(80)}...") 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") + // ── Step 4: Wait for contact convergence ──────────────────── + Log.i(tag, "Step 4: Waiting for contact convergence") var contactsA: List<ChatContact> = emptyList() var contactsB: List<ChatContact> = emptyList() withContext(Dispatchers.Default.limitedParallelism(1)) { @@ -193,33 +243,86 @@ class Gnunet1to1MessagingTest { contactsB = svcB.listContacts(handleB) val aHasB = contactsA.any { it.name.equals(nameB, ignoreCase = true) } val bHasA = contactsB.any { it.name.equals(nameA, ignoreCase = true) } + Log.d(tag, "poll: A.contacts=${contactsA.map { it.name }} B.contacts=${contactsB.map { it.name }}") if (aHasB && bHasA) break delay(500) } } } - Log.i(tag, "A contacts: ${contactsA.map { it.name }}") - Log.i(tag, "B contacts: ${contactsB.map { it.name }}") + Log.i(tag, "Step 4 OK: A.contacts=${contactsA.map { it.name }} B.contacts=${contactsB.map { it.name }}") + + assertTrue( + "A should have B as contact (got: ${contactsA.map { it.name }})", + contactsA.any { it.name.equals(nameB, ignoreCase = true) } + ) + assertTrue( + "B should have A as contact (got: ${contactsB.map { it.name }})", + contactsB.any { it.name.equals(nameA, ignoreCase = true) } + ) - // 5. Resolve the 1:1 contexts on each side. + // Wait for CADET transport to establish between A and B. Contact + // discovery (DHT/GNS) completes before the CADET channel is ready; + // sending immediately risks the message being silently dropped. + // Use Dispatchers.IO so the delay uses real wall-clock time — runTest + // uses a virtual scheduler that makes plain delay() instant. + Log.i(tag, "Step 4.5: waiting 15s for CADET transport to establish...") + withContext(Dispatchers.IO) { delay(15_000) } + Log.i(tag, "Step 4.5: CADET wait complete") + + // ── Step 5: Resolve 1:1 contexts ─────────────────────────── + Log.i(tag, "Step 5: Resolving 1:1 contexts") 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}" + + Log.i(tag, "Context A->B: nativePtr=${ctxAtoB.nativeContextPointer} userPtr=${ctxAtoB.userPointer}") + Log.i(tag, "Context B->A: nativePtr=${ctxBtoA.nativeContextPointer} userPtr=${ctxBtoA.userPointer}") + + assertNotNull( + "nativeContextPointer on A->B context must not be null", + ctxAtoB.nativeContextPointer ) + assertNotNull( + "nativeContextPointer on B->A context must not be null", + ctxBtoA.nativeContextPointer + ) + assertTrue( + "nativeContextPointer on A->B must be non-empty (was '${ctxAtoB.nativeContextPointer}')", + ctxAtoB.nativeContextPointer!!.isNotEmpty() + ) + assertTrue( + "nativeContextPointer on B->A must be non-empty (was '${ctxBtoA.nativeContextPointer}')", + ctxBtoA.nativeContextPointer!!.isNotEmpty() + ) + Log.i(tag, "Step 5 OK: nativeContextPointers validated") - // 6. Send A -> B and assert receipt on B. + // ── Step 6: Send A -> B and verify receipt ────────────────── val bodyAtoB = "ping-from-A-$ts-${(1000..9999).random()}" - Log.i(tag, "A.sendText -> '$bodyAtoB'") + Log.i(tag, "Step 6: A.sendText -> '$bodyAtoB'") svcA.sendText(ctxAtoB, bodyAtoB) + + // Give the native layer a moment to process, then verify the message + // was stored locally in A's context before waiting on B to receive it. + // Real-time delay — runTest's scheduler makes plain delay() virtual. + withContext(Dispatchers.IO) { delay(3_000) } + val localMsgsA = svcA.iterateContextMessages(ctxAtoB) + Log.i( + tag, + "Step 6 local verify: A's context has ${localMsgsA.size} TEXT message(s) " + + "after send: ${localMsgsA.map { "'${it.text}' kind=${it.kind}" }}" + ) + if (localMsgsA.none { it.text == bodyAtoB }) { + Log.e(tag, "Step 6 WARNING: sent message NOT found in A's local context — " + + "nativeContextSendText may have failed or CADET was not ready") + } + val receivedOnB = waitForIncomingText( receiverLabel = "B", receiverLog = logB, expectedText = bodyAtoB, - expectedSenderName = nameA + expectedSenderName = nameA, + timeoutMs = 120_000 ) assertEquals("Body received on B must equal body sent by A", bodyAtoB, receivedOnB.text) assertEquals( @@ -227,16 +330,31 @@ class Gnunet1to1MessagingTest { nameA.lowercase(), receivedOnB.sender?.name?.lowercase() ) + Log.i(tag, "Step 6 OK: A->B text delivered and verified") - // 7. Send B -> A and assert receipt on A. + // ── Step 7: Send B -> A and verify receipt ────────────────── val bodyBtoA = "pong-from-B-$ts-${(1000..9999).random()}" - Log.i(tag, "B.sendText -> '$bodyBtoA'") + Log.i(tag, "Step 7: B.sendText -> '$bodyBtoA'") svcB.sendText(ctxBtoA, bodyBtoA) + + withContext(Dispatchers.IO) { delay(3_000) } + val localMsgsB = svcB.iterateContextMessages(ctxBtoA) + Log.i( + tag, + "Step 7 local verify: B's context has ${localMsgsB.size} TEXT message(s) " + + "after send: ${localMsgsB.map { "'${it.text}' kind=${it.kind}" }}" + ) + if (localMsgsB.none { it.text == bodyBtoA }) { + Log.e(tag, "Step 7 WARNING: sent message NOT found in B's local context — " + + "nativeContextSendText may have failed or CADET was not ready") + } + val receivedOnA = waitForIncomingText( receiverLabel = "A", receiverLog = logA, expectedText = bodyBtoA, - expectedSenderName = nameB + expectedSenderName = nameB, + timeoutMs = 120_000 ) assertEquals("Body received on A must equal body sent by B", bodyBtoA, receivedOnA.text) assertEquals( @@ -244,7 +362,8 @@ class Gnunet1to1MessagingTest { nameB.lowercase(), receivedOnA.sender?.name?.lowercase() ) + Log.i(tag, "Step 7 OK: B->A text delivered and verified") Log.i(tag, "=== PASS: bidirectional 1:1 text delivery verified ===") } -} +} +\ No newline at end of file