PairingCodeTest.kt

3.8 KB · raw

1package com.peachpasswords.sync23import org.junit.Assert.assertEquals4import org.junit.Assert.assertNull5import org.junit.Test67/**8 * The phone is the only side that generates pairing codes and the browser is9 * the only side that reads them, so a drift here is invisible until a real10 * pairing fails with nothing to look at. The golden vector below was produced11 * by running the browser's own `shared/pairing-code.ts` — not derived from this12 * implementation — so it fails if either side moves.13 */14class PairingCodeTest {1516    // encodePairingCode("192.168.7.115", 49142, deadbeef) in peach-browser,17    // re-generated after both sides moved to a 4-byte token.18    private val vectorCode = "R2M0-EWXZ-YVFA-VFQF"19    private val vectorToken = byteArrayOf(0xde.toByte(), 0xad.toByte(), 0xbe.toByte(), 0xef.toByte())2021    /** The 8-byte code an older build would have shown. */22    private val legacyCode = "R2M0-EWXZ-YVFA-VFQF-04HM-ASR"2324    @Test fun `encodes the browser's golden vector`() {25        assertEquals(vectorCode, PairingCode.encode("192.168.7.115", 49142, vectorToken))26    }2728    @Test fun `parses the browser's golden vector`() {29        val parsed = PairingCode.parse(vectorCode)!!30        assertEquals("192.168.7.115", parsed.ip)31        assertEquals(49142, parsed.port)32        assertEquals("deadbeef", parsed.tokenHex)33    }3435    @Test fun `still decodes a code from an older eight-byte build`() {36        // The browser kept decoding lenient on purpose so a peer that hasn't37        // updated still pairs rather than hard-failing; match that.38        val parsed = PairingCode.parse(legacyCode)!!39        assertEquals("192.168.7.115", parsed.ip)40        assertEquals(49142, parsed.port)41        assertEquals("deadbeef01234567", parsed.tokenHex)42    }4344    @Test fun `round-trips a freshly generated token`() {45        val token = PairingCode.newToken()46        val parsed = PairingCode.parse(PairingCode.encode("10.0.0.7", 49142, token))!!47        assertEquals("10.0.0.7", parsed.ip)48        assertEquals(49142, parsed.port)49        assertEquals(token.joinToString("") { "%02x".format(it) }, parsed.tokenHex)50    }5152    @Test fun `folds the ambiguous glyphs a user might read off a screen`() {53        // I/L -> 1 and O -> 0, so the doc's own example resolves rather than54        // failing on characters Crockford's alphabet excludes.55        val typed = PairingCode.parse("r2mO-ewxz-yvfa-vfqf")56        assertEquals(PairingCode.parse(vectorCode), typed)57    }5859    @Test fun `rejects input that is not a pairing code`() {60        assertNull(PairingCode.parse(""))61        assertNull(PairingCode.parse("418207"))          // the 6-digit SAS62        assertNull(PairingCode.parse("not-a-code!!"))    // U is not in the alphabet63    }6465    @Test(expected = IllegalArgumentException::class)66    fun `refuses to encode a zero port`() {67        PairingCode.encode("10.0.0.1", 0, PairingCode.newToken())68    }6970    @Test fun `a generated code is four groups of four`() {71        // The length is what the user actually experiences, so pin it. Base3272        // is 5 bits per character, so this is fixed by TOKEN_BYTES — if that73        // grows, the code silently gains a group.74        val code = PairingCode.encode("192.168.1.20", 49142, PairingCode.newToken())75        assertEquals(listOf(4, 4, 4, 4), code.split("-").map { it.length })76        assertEquals(19, code.length)77    }7879    @Test(expected = IllegalArgumentException::class)80    fun `refuses a token shorter than the canonical size`() {81        PairingCode.encode("10.0.0.1", 49142, byteArrayOf(1, 2, 3))82    }8384    @Test(expected = IllegalArgumentException::class)85    fun `refuses a token longer than the canonical size`() {86        // The browser throws here too. A longer token encodes fine and quietly87        // produces a six-group code, which is the regression to prevent.88        PairingCode.encode("10.0.0.1", 49142, ByteArray(8))89    }90}