1package com.peachpasswords.sync23import org.junit.Assert.assertEquals4import org.junit.Assert.assertNotNull5import org.junit.Assert.assertNull6import org.junit.Test78class PairingInvitationTest {9 private val token = "AAECAwQFBgcICQoLDA0ODw"10 private val relay = "wss://relay.peachpasswords.com"11 private val staticKey = "01".repeat(32)12 private val kat =13 "peach-pair-v2:eyJyZWxheSI6IndzczovL3JlbGF5LnBlYWNocGFzc3dvcmRzLmNvbSIsInRva2VuIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EdyIsInZhdWx0IjoicG9wdWxhdGVkIn0"14 private val emptyKat =15 "peach-pair-v2:eyJyZWxheSI6IndzczovL3JlbGF5LnBlYWNocGFzc3dvcmRzLmNvbSIsInRva2VuIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EdyIsInZhdWx0IjoiZW1wdHkifQ"1617 @Test fun `matches browser invitation KAT with default populated vault`() {18 assertEquals(kat, createPairingInvitation("$relay/", token))19 val parsed = parsePairingInvitation(kat)20 assertEquals(PairingInvitation(relay, token, VaultStatus.POPULATED), parsed)21 assertEquals("$relay/r/$token", parsed!!.roomUrl())22 }2324 @Test fun `empty invitation round-trips as source role`() {25 assertEquals(emptyKat, createPairingInvitation(relay, token, VaultStatus.EMPTY))26 val parsed = parsePairingInvitation(emptyKat)27 assertEquals(PairingInvitation(relay, token, VaultStatus.EMPTY), parsed)28 }2930 @Test fun `explicit populated invitation round-trips as receiver role`() {31 val invitation = createPairingInvitation(relay, token, VaultStatus.POPULATED)32 val parsed = parsePairingInvitation(invitation)33 assertEquals(VaultStatus.POPULATED, parsed?.vaultStatus)34 }3536 @Test fun `Noise static key round-trips and malformed keys fail closed`() {37 val invitation = createPairingInvitation(38 relay,39 token,40 VaultStatus.POPULATED,41 staticKey,42 )43 assertEquals(staticKey, parsePairingInvitation(invitation)?.noiseStaticPublicKeyHex)44 assertNull(parsePairingInvitation(raw(45 """{"relay":"$relay","token":"$token","vault":"populated","static":"bad"}""",46 )))47 }4849 @Test fun `missing vault field defaults to populated for historical compatibility`() {50 val older = raw("""{"relay":"$relay","token":"$token"}""")51 val parsed = parsePairingInvitation(older)52 assertEquals(VaultStatus.POPULATED, parsed?.vaultStatus)53 assertEquals(PairingInvitation(relay, token), parsed)54 }5556 @Test fun `unknown vault value defaults to populated`() {57 val weird = raw("""{"relay":"$relay","token":"$token","vault":"maybe"}""")58 val parsed = parsePairingInvitation(weird)59 assertEquals(VaultStatus.POPULATED, parsed?.vaultStatus)60 }6162 @Test fun `vaultStatus on the parsed invitation drives the adoption flow`() {63 val empty = parsePairingInvitation(emptyKat)64 val populated = parsePairingInvitation(kat)65 val legacy = parsePairingInvitation(raw("""{"relay":"$relay","token":"$token"}"""))66 assertNotNull(empty)67 assertNotNull(populated)68 assertNotNull(legacy)69 assertEquals(VaultStatus.EMPTY, empty!!.vaultStatus)70 assertEquals(VaultStatus.POPULATED, populated!!.vaultStatus)71 assertEquals(VaultStatus.POPULATED, legacy!!.vaultStatus)72 }7374 @Test fun `rejects malformed insecure and short-token invitations`() {75 assertNull(parsePairingInvitation("not-an-invitation"))76 assertNull(parsePairingInvitation("peach-pair-v2:%%%"))77 assertNull(parsePairingInvitation(raw("""{"relay":"ws://relay.test","token":"$token"}""")))78 assertNull(parsePairingInvitation(raw("""{"relay":"$relay","token":"short"}""")))79 }8081 private fun raw(json: String): String =82 "peach-pair-v2:" + java.util.Base64.getUrlEncoder().withoutPadding()83 .encodeToString(json.toByteArray())84}