diff --git a/engine/src/test/kotlin/io/flamenet/e2e/MailboxStateTest.kt b/engine/src/test/kotlin/io/flamenet/e2e/MailboxStateTest.kt new file mode 100644 index 0000000..1ce2c9a --- /dev/null +++ b/engine/src/test/kotlin/io/flamenet/e2e/MailboxStateTest.kt @@ -0,0 +1,183 @@ +package io.flamenet.e2e + +import javax.crypto.KeyGenerator +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * What the vault remembers about several mailboxes. + * + * 🔴 **Two of these keys are chosen by whoever writes to you.** An envelope id + * comes from the sender, so an unbounded list of them is storage anybody on the + * federation can grow — and a relay name comes from a signed record, so a + * per-relay entry can be created by anyone who can get a record published. Both + * are bounded or scoped deliberately, and that is worth asserting rather than + * trusting to a comment. + */ +class MailboxStateTest { + + private class MapStorage : Vault.Storage { + val records = LinkedHashMap() + override fun read(name: String) = records[name] + override fun write(name: String, bytes: ByteArray) { records[name] = bytes } + override fun delete(name: String) { records.remove(name) } + override fun names() = records.keys.toList() + } + + private val storage = MapStorage() + /// Held so a SECOND vault can be opened over the same storage — which is + /// what a restart is, and the only way to test that anything persisted. + private val key = KeyGenerator.getInstance("AES").apply { init(256) }.generateKey() + private val vault = Vault(storage, key) + + // MARK: - Envelope ids + + @Test + fun `an envelope is remembered once and recognised again`() { + assertFalse(vault.hasSeenEnvelope("env-1")) + vault.rememberEnvelope("env-1") + assertTrue(vault.hasSeenEnvelope("env-1")) + assertFalse(vault.hasSeenEnvelope("env-2")) + } + + /** + * It survives a restart, or a copy at a backup home is read twice on the + * first poll after every launch. + * + * ⚠️ Written with one vault and read with a SECOND over the same storage, + * which is what a restart actually is. The first version of this test + * asserted that two objects were not the same object, which is true of any + * two objects and proves nothing about persistence. + */ + @Test + fun `what was seen survives reopening the vault`() { + vault.rememberEnvelope("env-1") + + val restarted = Vault(storage, key) + assertTrue(restarted.hasSeenEnvelope("env-1"), "the dedupe list did not persist") + assertFalse(restarted.hasSeenEnvelope("env-2")) + } + + /** The same for everything else a mailbox needs across a launch. */ + @Test + fun `mailboxes and their cursors survive reopening the vault`() { + vault.saveExtraHomes(listOf(Vault.ExtraHome("a.example.com", "me", "pw"))) + vault.setWatermarkFor("a.example.com", 900) + vault.rememberKeyLogServed("a.example.com") + + val restarted = Vault(storage, key) + assertEquals(listOf("a.example.com"), restarted.extraHomes().map { it.relay }) + assertEquals(900, restarted.watermarkFor("a.example.com")) + assertTrue(restarted.keyLogServed("a.example.com")) + } + + /** + * 🔴 **The key comes from whoever wrote to us.** Oldest first, so a + * forgotten id can at worst show a very old message twice — far better than + * a list anybody can grow without limit. + */ + @Test + fun `the list of seen envelopes is bounded`() { + for (i in 0 until 2_500) vault.rememberEnvelope("env-$i") + + assertTrue(vault.hasSeenEnvelope("env-2499"), "the newest must be remembered") + assertFalse(vault.hasSeenEnvelope("env-0"), "the list grew without limit") + } + + @Test + fun `remembering the same id twice does not grow the list`() { + repeat(50) { vault.rememberEnvelope("env-same") } + vault.rememberEnvelope("env-other") + assertTrue(vault.hasSeenEnvelope("env-same")) + assertTrue(vault.hasSeenEnvelope("env-other")) + } + + // MARK: - Per-relay state + + /** + * 🔴 Envelope ids come from each relay's own database, so id 40 there says + * nothing about id 40 here. One shared cursor would let the first mailbox's + * progress silently skip the second's mail. + */ + @Test + fun `each relay keeps its own watermark`() { + vault.setWatermarkFor("a.example.com", 900) + assertEquals(900, vault.watermarkFor("a.example.com")) + assertEquals(0, vault.watermarkFor("b.example.com")) + assertEquals(0, vault.inboxWatermark, "the signed-in relay's cursor is separate again") + } + + /** + * 🔴 A single global flag meant that signing in to a second relay which + * keeps no log read as the FIRST going quiet — a fork warning against an + * honest deployment, and every contact on it unreachable. + */ + @Test + fun `the served flag is per relay`() { + assertFalse(vault.keyLogServed("a.example.com")) + vault.rememberKeyLogServed("a.example.com") + assertTrue(vault.keyLogServed("a.example.com")) + assertFalse(vault.keyLogServed("b.example.com")) + } + + /** + * ⚠️ The pin falls back to the pre-2026-08-30 global entry, so a phone + * already holding one keeps it rather than starting trust-on-first-use + * again — and a per-relay entry, once written, wins. + */ + @Test + fun `an existing global pin is inherited until a per-relay one is written`() { + vault.saveKeyLogPin(Vault.KeyLogPin(4, "old-root")) + assertEquals("old-root", vault.loadKeyLogPin("a.example.com")?.root) + + vault.saveKeyLogPin(Vault.KeyLogPin(9, "new-root"), "a.example.com") + assertEquals("new-root", vault.loadKeyLogPin("a.example.com")?.root) + assertEquals("old-root", vault.loadKeyLogPin("b.example.com")?.root, + "another relay still inherits the legacy pin, not this one") + } + + // MARK: - Extra mailboxes + + /** + * ⚠️ **A credential per relay, not one shared.** An app password is minted + * by the relay that issued it and refused everywhere else, so a single + * stored credential authenticates at exactly one home and silently fails at + * the rest — which reads as "the backup mailbox never has anything in it". + */ + @Test + fun `each mailbox keeps its own credential and dial url`() { + vault.saveExtraHomes(listOf( + Vault.ExtraHome("a.example.com", "me", "pw-a"), + Vault.ExtraHome("b.example.com", "me", "pw-b", "http://127.0.0.1:9"), + )) + + val homes = vault.extraHomes() + assertEquals(listOf("a.example.com", "b.example.com"), homes.map { it.relay }) + assertEquals(listOf("pw-a", "pw-b"), homes.map { it.appPassword }) + // The record names a hostname and cannot carry a port; how this device + // dials that host is a separate field with a sensible default. + assertEquals("https://a.example.com", homes[0].baseUrl) + assertEquals("http://127.0.0.1:9", homes[1].baseUrl) + } + + /** Dropping one takes its cursor with it, or a re-add resumes mid-inbox. */ + @Test + fun `forgetting a mailbox clears its cursor too`() { + vault.saveExtraHomes(listOf(Vault.ExtraHome("a.example.com", "me", "pw"))) + vault.setWatermarkFor("a.example.com", 900) + + vault.forgetExtraHome("a.example.com") + + assertTrue(vault.extraHomes().isEmpty()) + assertEquals(0, vault.watermarkFor("a.example.com"), + "a re-added mailbox would resume mid-inbox and skip everything before it") + } + + @Test + fun `an account with no extra mailboxes reads as none`() { + assertTrue(vault.extraHomes().isEmpty()) + assertEquals(0, vault.watermarkFor("anything.example.com")) + } +}