commit 51e33eccd6f0ef4bb8d87140e5755925369d4296
parent 6db3d671e3acee5b553bf7eb62f67dcae3ef52e9
Author: t3sserakt <t3ss@posteo.de>
Date: Tue, 12 May 2026 16:03:33 +0200
WIP: one to one test
Diffstat:
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 ===")
+ }
+}