Test address parsing, mirroring the Swift suite case for case

An address is parsed from input nobody authenticated: typed, pasted,
scanned from a code somebody else printed, and — worst — read out of a
sealed envelope written by a stranger on another relay. So the refusals
matter more than the successes.

Written to mirror the Swift cases one for one, because 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.

It found one on the first run, in the other direction: Swift's decoder
splits on the first `@` with `maxSplits: 1` and then refuses `/` and `:`
but not `@` — so `fn1abc@a@b.example.com` parsed as a relay of
`a@b.example.com`, a "hostname" no host is called. Fixed there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-08-30 17:51:12 -04:00
co-authored by Claude Opus 4.8
parent 088fd5f5a7
commit ca49eedade
@@ -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")
}
}
}