commit 2945805313541f21949712cfe49f17c6650fdef0
parent f9b609a1cbad55c071894f755eb228a92851089b
Author: t3sserakt <t3ss@posteo.de>
Date: Mon, 25 May 2026 20:45:56 +0200
one to one message fixed
Diffstat:
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