package com.peachpasswords.sync import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Test /** * Regression coverage for M-08: the hand-rolled PAIR_IDENTITY encoder escaped * `"`, `\`, and control bytes; the parser stopped at the next unescaped quote * and never unescaped. Names containing those characters truncated the frame * and corrupted everything after it. The kotlinx.serialization-backed codec * round-trips every JSON escape and validates fields at the trust boundary. */ private const val MAX_DEVICE_NAME_FOR_TEST = 64 class PairIdentityFramesTest { private val base = LocalDeviceIdentity( deviceId = "client-1", deviceName = "Phone", deviceType = DeviceType.ANDROID, longTermPublicKeyHex = "cd".repeat(32), signingPublicKeyHex = "ef".repeat(32), relayUrl = "wss://relay.peachpasswords.com", ) @Test fun `round-trips a device name containing every JSON escape character`() { val tricky = "a\"b\\c\nd\te\rf" val wire = encodePairIdentityFrame(base.copy(deviceName = tricky)) val parsed = parsePairIdentityFrame(wire) assertEquals(tricky, parsed.deviceName) } @Test fun `decoder rejects a name with a raw embedded newline`() { val raw = """{"type":"PAIR_IDENTITY","device_id":"d","device_name":"bad${"\n"}name","device_type":"android","long_term_public_key_hex":"${"cd".repeat(32)}","signing_public_key_hex":"${"ef".repeat(32)}","relay_url":"wss://r.example.com"}""" try { parsePairIdentityFrame(raw.toByteArray()) fail("expected control-char rejection") } catch (e: IllegalArgumentException) { assertTrue(e.message!!.contains("device_name")) } } @Test fun `decoder unescapes a literal backslash-quote in the name`() { // The legacy parser stopped at the first unescaped quote and read // `a\` as the value, then desynced the rest of the frame. val raw = """{"type":"PAIR_IDENTITY","device_id":"d","device_name":"a\"b","device_type":"android","long_term_public_key_hex":"${"cd".repeat(32)}","signing_public_key_hex":"${"ef".repeat(32)}","relay_url":"wss://r.example.com"}""" val parsed = parsePairIdentityFrame(raw.toByteArray()) assertEquals("a\"b", parsed.deviceName) } @Test fun `decoder rejects a non-hex long term public key`() { val raw = """{"type":"PAIR_IDENTITY","device_id":"d","device_name":"n","device_type":"android","long_term_public_key_hex":"${"zz".repeat(32)}","signing_public_key_hex":"${"ef".repeat(32)}","relay_url":"wss://r.example.com"}""" try { parsePairIdentityFrame(raw.toByteArray()) fail("expected hex rejection") } catch (e: IllegalArgumentException) { assertTrue(e.message!!.contains("hex")) } } @Test fun `decoder rejects a device name longer than the cap`() { val long = "x".repeat(MAX_DEVICE_NAME_FOR_TEST + 1) val wire = encodePairIdentityFrame(base.copy(deviceName = long)) try { parsePairIdentityFrame(wire) fail("expected length rejection") } catch (e: IllegalArgumentException) { assertTrue(e.message!!.contains("device_name")) } } @Test fun `signing key is null when omitted from the wire`() { val omitted = """{"type":"PAIR_IDENTITY","device_id":"d","device_name":"n","device_type":"android","long_term_public_key_hex":"${"cd".repeat(32)}","relay_url":"wss://r.example.com"}""" val parsed = parsePairIdentityFrame(omitted.toByteArray()) assertNull(parsed.signingPublicKeyHex) } @Test fun `wire format preserves the type discriminator and snake_case keys`() { val wire = String(encodePairIdentityFrame(base), Charsets.UTF_8) assertTrue("frame must include type discriminator", wire.contains("\"type\":\"PAIR_IDENTITY\"")) assertTrue("frame must use snake_case keys", wire.contains("\"device_id\"")) assertTrue("frame must use snake_case keys", wire.contains("\"long_term_public_key_hex\"")) } }