diff --git a/engine/src/test/kotlin/io/flamenet/e2e/ContactCardTest.kt b/engine/src/test/kotlin/io/flamenet/e2e/ContactCardTest.kt new file mode 100644 index 0000000..e14531e --- /dev/null +++ b/engine/src/test/kotlin/io/flamenet/e2e/ContactCardTest.kt @@ -0,0 +1,116 @@ +package io.flamenet.e2e + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Parsing an address, and refusing everything that is nearly one. + * + * 🔴 **This runs on input nobody authenticated.** An address arrives typed, or + * pasted, or scanned from a code somebody else printed — and, worst of all, + * from inside a sealed envelope written by a stranger on another relay. So the + * refusals matter more than the successes: accepting `fnid@https://x/y` would + * let a message point this client at a path on a host of the sender's choosing. + * + * ⚠️ The cases here deliberately mirror the Swift suite one for one. Two + * implementations that disagree about which strings are addresses open two + * threads for one person on one side and none on the other, and nothing about + * either engine's own tests would show it. + */ +class ContactCardTest { + + @Test + fun `a bare address with no scheme is accepted`() { + val card = ContactCard.decode("fn1abc@relay1.flamenet.io") + assertEquals("fn1abc", card?.fnid) + assertEquals("relay1.flamenet.io", card?.relay) + assertNull(card?.deliveryKey, "no key means it cannot be sealed, and that is knowable") + } + + @Test + fun `the scheme is stripped and the host lowercased`() { + val card = ContactCard.decode(" FLAMENET:fn1abc@Relay1.Flamenet.IO ") + assertEquals("fn1abc", card?.fnid) + assertEquals("relay1.flamenet.io", card?.relay) + assertEquals("fn1abc@relay1.flamenet.io", card?.address) + } + + @Test + fun `what it encodes parses back to the same address`() { + val card = ContactCard("fn1abc", "relay1.flamenet.io") + assertEquals("flamenet:fn1abc@relay1.flamenet.io", card.encoded) + assertEquals(card.address, ContactCard.decode(card.encoded)?.address) + } + + /** + * ⚠️ Kept only so an older card still parses. Nothing reads it and nothing + * should start to: sealing uses the recipient DEVICE's key, which the + * remote directory returns, and the federated send carries no delivery key + * at all. + */ + @Test + fun `an older card carrying a delivery key still parses`() { + val card = ContactCard.decode("flamenet:fn1abc@relay1.flamenet.io?k=" + "A".repeat(44)) + assertEquals("fn1abc", card?.fnid) + assertEquals("relay1.flamenet.io", card?.relay) + assertEquals("A".repeat(44), card?.deliveryKey) + // And it is NOT re-emitted, because publishing a secret-shaped thing + // for nothing doubles the length of every address. + assertTrue("?" !in card!!.encoded, card.encoded) + } + + /** + * 🔴 A relay is a hostname. Accepting a URL here would let a card — or a + * message from a stranger — point this client at a path on somebody else's + * host. + */ + @Test + fun `a relay that is a URL is refused`() { + assertNull(ContactCard.decode("fn1@https://evil.example.com/path")) + assertNull(ContactCard.decode("fn1@evil.example.com/path")) + assertNull(ContactCard.decode("fn1@evil.example.com:8443")) + } + + @Test + fun `the nearly right is refused`() { + assertNull(ContactCard.decode("")) + assertNull(ContactCard.decode("fn1abc"), "no relay at all") + assertNull(ContactCard.decode("@relay1.flamenet.io"), "no address") + assertNull(ContactCard.decode("fn1abc@"), "no relay") + assertNull(ContactCard.decode("fn1abc@localhost"), "a hostname needs a dot") + } + + /** + * ⚠️ A second `@` means this is not one address. Splitting on the first + * would silently address a different host than the string names. + */ + @Test + fun `two at signs is not one address`() { + assertNull(ContactCard.decode("fn1abc@a@b.example.com")) + } + + /** + * 🔴 Two spellings of one address would open two threads for one person, so + * everything that files a message goes through the same normaliser. + */ + @Test + fun `canonical gives one spelling for one address`() { + val spellings = listOf( + "fn1abc@relay1.flamenet.io", + "flamenet:fn1abc@relay1.flamenet.io", + " fn1abc@RELAY1.Flamenet.io ", + "flamenet:fn1abc@relay1.flamenet.io?k=" + "A".repeat(44), + ) + val canonical = spellings.map { ContactCard.canonical(it) }.toSet() + assertEquals(setOf("fn1abc@relay1.flamenet.io"), canonical, "got $canonical") + } + + @Test + fun `canonical refuses what decode refuses`() { + for (bad in listOf("", "fn1abc", "fn1abc@localhost", "fn1@evil.example.com/x")) { + assertNull(ContactCard.canonical(bad), "accepted $bad") + } + } +}