package com.peachpasswords.sync import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Test /** * PB-26-205 (Android mirror): the pairing/adoption relay inbox is bounded * before any allocate/copy of attacker-controlled frame data, with the same * ceilings as the browser (`src/shared/pairing-dial.ts`). Both clients must * agree on accept/reject at these boundaries. */ class PairingRelayLimitsTest { private fun inbox(limitCalls: MutableList = mutableListOf()) = RelayPairingInbox { limitCalls.add(1) } @Test fun `shared ceilings match the browser vectors`() { assertEquals(1024 * 1024, MAX_PAIRING_FRAME_BYTES) assertEquals(12 * 1024 * 1024, MAX_PAIRING_INBOX_BYTES) assertEquals(256, MAX_PAIRING_INBOX_MESSAGES) } @Test fun `max frame is accepted intact`() = runBlocking { val inbox = inbox() val frame = ByteArray(MAX_PAIRING_FRAME_BYTES) { (it % 7).toByte() } assertTrue(inbox.onFrame(frame.size) { frame.copyOf() }) assertArrayEquals(frame, inbox.receive()) } @Test fun `max-plus-one is refused before the copy`() = runBlocking { val limitCalls = mutableListOf() val inbox = inbox(limitCalls) var copied = false assertFalse(inbox.onFrame(MAX_PAIRING_FRAME_BYTES + 1) { copied = true; ByteArray(0) }) assertFalse("oversize frame must be rejected before read/copy", copied) assertEquals(1, limitCalls.size) try { inbox.receive() fail("expected PairingResourceLimitException") } catch (error: PairingResourceLimitException) { assertEquals("pairing_resource_limit", error.message) } } @Test fun `individually valid frames tripping the byte budget close the inbox`() = runBlocking { val limitCalls = mutableListOf() val inbox = inbox(limitCalls) val frames = MAX_PAIRING_INBOX_BYTES / MAX_PAIRING_FRAME_BYTES repeat(frames) { assertTrue(inbox.onFrame(MAX_PAIRING_FRAME_BYTES) { ByteArray(MAX_PAIRING_FRAME_BYTES) }) } assertFalse(inbox.onFrame(1) { ByteArray(1) }) assertEquals(1, limitCalls.size) try { inbox.receive() fail("expected PairingResourceLimitException") } catch (_: PairingResourceLimitException) { } } @Test fun `tiny frames trip the message-count limit`() = runBlocking { val limitCalls = mutableListOf() val inbox = inbox(limitCalls) repeat(MAX_PAIRING_INBOX_MESSAGES) { assertTrue(inbox.onFrame(1) { ByteArray(1) }) } assertTrue(limitCalls.isEmpty()) assertFalse(inbox.onFrame(1) { ByteArray(1) }) assertEquals(1, limitCalls.size) try { inbox.receive() fail("expected PairingResourceLimitException") } catch (_: PairingResourceLimitException) { } } @Test fun `dequeue relieves the budget for later valid frames`() = runBlocking { val limitCalls = mutableListOf() val inbox = inbox(limitCalls) repeat(MAX_PAIRING_INBOX_MESSAGES) { assertTrue(inbox.onFrame(1) { ByteArray(1) }) } assertEquals(1, inbox.receive().size) assertTrue(inbox.onFrame(1) { ByteArray(1) }) assertTrue(limitCalls.isEmpty()) repeat(MAX_PAIRING_INBOX_MESSAGES) { inbox.receive() } } @Test fun `pending readers share one terminal error and later frames are ignored`() = runBlocking { val limitCalls = mutableListOf() val inbox = inbox(limitCalls) val first = async { try { inbox.receive() null } catch (error: PairingResourceLimitException) { error } } val second = async { try { inbox.receive() null } catch (error: PairingResourceLimitException) { error } } // Let both readers park, then trip the limit. launch { inbox.onFrame(MAX_PAIRING_FRAME_BYTES + 1) { ByteArray(0) } }.join() assertEquals("pairing_resource_limit", first.await()?.message) assertEquals("pairing_resource_limit", second.await()?.message) assertFalse(inbox.onFrame(1) { ByteArray(1) }) assertEquals(1, limitCalls.size) try { inbox.receive() fail("expected PairingResourceLimitException") } catch (_: PairingResourceLimitException) { } } }