package com.peachpasswords.sync import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertSame import org.junit.Assert.assertThrows import org.junit.Test import java.security.SecureRandom class RendezvousTest { private val handshakeHash = Rendezvous.secretFromHex( "0fbab8cd1782bf4b17d69beb69f5b80b0bf24d93da4924959c76ee701c32e655", ) @Test fun `matches browser rendezvous KATs`() { val secret = Rendezvous.deriveSecret(handshakeHash) assertEquals( "0803691c8ac7330883c0987e33cea4a171992366ce4b06b88691e84ca689773d", Rendezvous.bytesToHex(secret), ) assertEquals( "9UY3j5AHnwYSApvymtY965su9n60rQOv8QZ2PZzHuKI", Rendezvous.deriveId(secret, 500_000), ) } @Test fun `uses required waiting and dialing window order`() { val secret = Rendezvous.deriveSecret(handshakeHash) val now = 500_000L * 3600 + 3599 assertEquals( listOf( Rendezvous.deriveId(secret, 500_000), Rendezvous.deriveId(secret, 499_999), ), Rendezvous.waitingIds(secret, now), ) assertEquals( listOf( Rendezvous.deriveId(secret, 500_000), Rendezvous.deriveId(secret, 499_999), Rendezvous.deriveId(secret, 500_001), ), Rendezvous.dialingIds(secret, now), ) } @Test fun `opaque capabilities resolve current and previous windows only`() { val secretHex = "42".repeat(32) val peer = PairedDevice( deviceId = "browser-1", longTermPublicKeyHex = "ab".repeat(32), pairedAt = 1L, rendezvousSecretHex = secretHex, ) val legacy = PairedDevice( deviceId = "legacy", longTermPublicKeyHex = "cd".repeat(32), pairedAt = 1L, ) val secret = Rendezvous.secretFromHex(secretHex) val now = 500_000L * 3600 + 3599 val current = Rendezvous.deriveId(secret, 500_000) val previous = Rendezvous.deriveId(secret, 499_999) val next = Rendezvous.deriveId(secret, 500_001) assertSame(peer, Rendezvous.resolvePeer(listOf(legacy, peer), current, now)) assertSame(peer, Rendezvous.resolvePeer(listOf(legacy, peer), previous, now)) assertNull(Rendezvous.resolvePeer(listOf(legacy, peer), next, now)) assertNull(Rendezvous.resolvePeer(listOf(legacy, peer), "Z".repeat(43), now)) assertNull(Rendezvous.resolvePeer(listOf(legacy), current, now)) } @Test fun `first-pair token is 128-bit base64url`() { val deterministic = object : SecureRandom() { override fun nextBytes(bytes: ByteArray) { bytes.indices.forEach { bytes[it] = it.toByte() } } } assertEquals("AAECAwQFBgcICQoLDA0ODw", Rendezvous.newPairingToken(deterministic)) } @Test fun `relay URL requires credential-free wss`() { assertEquals( "wss://relay.example.test/base", Rendezvous.normalizeRelayUrl("wss://relay.example.test/base/"), ) assertThrows(IllegalArgumentException::class.java) { Rendezvous.normalizeRelayUrl("ws://relay.example.test") } assertThrows(IllegalArgumentException::class.java) { Rendezvous.normalizeRelayUrl("wss://user:pass@relay.example.test") } } @Test fun `completed pairing persists secret and the listener relay`() { val device = PairedDevice( deviceId = "browser-1", deviceName = "Firefox", deviceType = "extension", longTermPublicKeyHex = "ab".repeat(32), pairedAt = 1L, ) val bound = Rendezvous.bindPairState( device, handshakeHash, "wss://relay.example.test/", ) assertEquals( "0803691c8ac7330883c0987e33cea4a171992366ce4b06b88691e84ca689773d", bound.rendezvousSecretHex, ) assertEquals("wss://relay.example.test", bound.relayUrl) } }