This commit is contained in:
callebtc
2026-07-26 13:02:35 +02:00
parent eb55cdb6a7
commit 58ff730c9e
8 changed files with 335 additions and 112 deletions
@@ -0,0 +1,45 @@
package com.bitchat.android.mesh
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class BluetoothConnectionManagerTest {
private lateinit var manager: BluetoothConnectionManager
@Before
fun setUp() {
manager = BluetoothConnectionManager(
RuntimeEnvironment.getApplication(),
"0011223344556677"
)
}
@After
fun tearDown() {
manager.stopServices()
}
@Test
fun `inactive manager rejects a broadcast instead of reporting it queued`() {
val packet = BitchatPacket(
version = 1u,
type = MessageType.MESSAGE.value,
senderID = byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7),
recipientID = null,
timestamp = 1uL,
payload = byteArrayOf(1),
ttl = 7u
)
assertFalse(manager.broadcastPacket(RoutedPacket(packet)))
}
}
@@ -160,6 +160,51 @@ class PrivateMediaTransferPreparerTest {
assertTrue(!fragmented)
}
@Test
fun `impossible content size rejects before policy encryption signing or fragmentation`() {
var policyChecked = false
var encrypted = false
var finalized = false
var fragmented = false
val absolutePayloadUpperBound =
com.bitchat.android.util.AppConstants.Fragmentation.MAX_FRAGMENTS_PER_ID *
com.bitchat.android.util.AppConstants.Fragmentation.MAX_FRAGMENT_SIZE
val preparer = PrivateMediaTransferPreparer(
senderID = senderID,
ttl = 7u,
policyProvider = {
policyChecked = true
PrivateMediaPolicyDecision.Encrypted(authenticatedSession)
},
encrypt = { _, _, _ ->
encrypted = true
PrivateMediaEncryptionResult.Success(byteArrayOf(1))
},
finalizeRoutedAndSigned = {
finalized = true
it
},
fragment = { _, _ ->
fragmented = true
emptyList()
}
)
val outcome = preparer.prepare(
"peer",
recipientID,
file(absolutePayloadUpperBound + 1),
false
)
assertTrue(outcome is PrivateMediaBuildOutcome.Rejected)
assertTrue((outcome as PrivateMediaBuildOutcome.Rejected).reason.contains("256"))
assertTrue(!policyChecked)
assertTrue(!encrypted)
assertTrue(!finalized)
assertTrue(!fragmented)
}
@Test
fun `no route accepts 256 final fragments and rejects 257`() {
assertExactBoundary(route = null)
@@ -7,6 +7,7 @@ import com.bitchat.android.mesh.PrivateMediaWireMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@@ -23,7 +24,11 @@ import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
@RunWith(RobolectricTestRunner::class)
class MediaSendingManagerMigrationTest {
@@ -45,6 +50,7 @@ class MediaSendingManagerMigrationTest {
MessageManager(state),
mock(),
CoroutineScope(SupervisorJob() + Dispatchers.Unconfined),
mediaWorkDispatcher = Dispatchers.Unconfined,
getMeshService = { mesh }
)
file = kotlin.io.path.createTempFile("private-media", ".jpg").toFile().apply {
@@ -72,6 +78,40 @@ class MediaSendingManagerMigrationTest {
verify(mesh, never()).sendFilePrivate(any(), any())
}
@Test
fun `private preparation runs on the configured media worker`() {
val executor = Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "private-media-test-worker")
}
val dispatcher = executor.asCoroutineDispatcher()
try {
val preparationThread = AtomicReference<String>()
val prepared = CountDownLatch(1)
whenever(mesh.prepareFilePrivate(eq(peerID), any(), any(), eq(false)))
.thenAnswer {
preparationThread.set(Thread.currentThread().name)
prepared.countDown()
PrivateMediaPreparation.Rejected("test complete")
}
val asynchronousManager = MediaSendingManager(
state,
MessageManager(state),
mock(),
CoroutineScope(SupervisorJob() + Dispatchers.Unconfined),
mediaWorkDispatcher = dispatcher,
getMeshService = { mesh }
)
asynchronousManager.sendImageNote(peerID, null, file.absolutePath)
assertTrue(prepared.await(5, TimeUnit.SECONDS))
assertTrue(preparationThread.get().contains("private-media-test-worker"))
} finally {
dispatcher.close()
executor.shutdownNow()
}
}
@Test
fun `pinned downgrade rejection is visible without a file echo or send`() {
whenever(mesh.prepareFilePrivate(eq(peerID), any(), any(), eq(false)))
@@ -225,6 +265,28 @@ class MediaSendingManagerMigrationTest {
assertTrue(state.privateChats.value[peerID].isNullOrEmpty())
}
@Test
fun `failed prepared commit rolls back the local file echo`() {
whenever(mesh.prepareFilePrivate(eq(peerID), any(), any(), eq(false)))
.thenAnswer { invocation ->
PrivateMediaPreparation.Ready(
PreparedPrivateMediaTransfer(
transferId = invocation.getArgument(2),
wireMode = PrivateMediaWireMode.ENCRYPTED_NOISE_0X20
) {
false
}
)
}
manager.sendImageNote(peerID, null, file.absolutePath)
val messages = state.privateChats.value[peerID].orEmpty()
assertEquals(1, messages.size)
assertTrue(messages.single().content.contains("could not be committed"))
assertTrue(messages.none { it.type == com.bitchat.android.model.BitchatMessageType.Image })
}
@Test
fun `cancelled consent cannot later send or echo`() {
whenever(mesh.prepareFilePrivate(eq(peerID), any(), any(), eq(false)))