1package com.peachpasswords.sync23import org.junit.Assert.assertEquals4import org.junit.Assert.assertNull5import org.junit.Assert.assertTrue6import org.junit.Assert.fail7import org.junit.Test8910111213141516private const val MAX_DEVICE_NAME_FOR_TEST = 641718class PairIdentityFramesTest {19 private val base = LocalDeviceIdentity(20 deviceId = "client-1",21 deviceName = "Phone",22 deviceType = DeviceType.ANDROID,23 longTermPublicKeyHex = "cd".repeat(32),24 signingPublicKeyHex = "ef".repeat(32),25 relayUrl = "wss://relay.peachpasswords.com",26 )2728 @Test29 fun `round-trips a device name containing every JSON escape character`() {30 val tricky = "a\"b\\c\nd\te\rf"31 val wire = encodePairIdentityFrame(base.copy(deviceName = tricky))32 val parsed = parsePairIdentityFrame(wire)33 assertEquals(tricky, parsed.deviceName)34 }3536 @Test37 fun `decoder rejects a name with a raw embedded newline`() {38 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"}"""39 try {40 parsePairIdentityFrame(raw.toByteArray())41 fail("expected control-char rejection")42 } catch (e: IllegalArgumentException) {43 assertTrue(e.message!!.contains("device_name"))44 }45 }4647 @Test48 fun `decoder unescapes a literal backslash-quote in the name`() {49 50 51 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"}"""52 val parsed = parsePairIdentityFrame(raw.toByteArray())53 assertEquals("a\"b", parsed.deviceName)54 }5556 @Test57 fun `decoder rejects a non-hex long term public key`() {58 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"}"""59 try {60 parsePairIdentityFrame(raw.toByteArray())61 fail("expected hex rejection")62 } catch (e: IllegalArgumentException) {63 assertTrue(e.message!!.contains("hex"))64 }65 }6667 @Test68 fun `decoder rejects a device name longer than the cap`() {69 val long = "x".repeat(MAX_DEVICE_NAME_FOR_TEST + 1)70 val wire = encodePairIdentityFrame(base.copy(deviceName = long))71 try {72 parsePairIdentityFrame(wire)73 fail("expected length rejection")74 } catch (e: IllegalArgumentException) {75 assertTrue(e.message!!.contains("device_name"))76 }77 }7879 @Test80 fun `signing key is null when omitted from the wire`() {81 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"}"""82 val parsed = parsePairIdentityFrame(omitted.toByteArray())83 assertNull(parsed.signingPublicKeyHex)84 }8586 @Test87 fun `wire format preserves the type discriminator and snake_case keys`() {88 val wire = String(encodePairIdentityFrame(base), Charsets.UTF_8)89 assertTrue("frame must include type discriminator",90 wire.contains("\"type\":\"PAIR_IDENTITY\""))91 assertTrue("frame must use snake_case keys", wire.contains("\"device_id\""))92 assertTrue("frame must use snake_case keys", wire.contains("\"long_term_public_key_hex\""))93 }94}