Prove an inbound edit is applied (relay#46) #88
@@ -0,0 +1,125 @@
|
||||
package io.flamenet.messenger.app
|
||||
|
||||
import io.flamenet.client.MessengerApi
|
||||
import io.flamenet.client.RelayClient
|
||||
import io.flamenet.e2e.DeviceIdentity
|
||||
import io.flamenet.e2e.Vault
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
/**
|
||||
* An inbound edit changes the message it names (§6.15).
|
||||
*
|
||||
* 🔴 **Written to settle a claim, not to add coverage.** The published spec
|
||||
* says Android *"cannot yet apply one, because its transcript does not record a
|
||||
* `mid` for each line"*, and that both blocks replies and reactions too. The
|
||||
* transcript does record a `mid` — `MessageReactionTest` folds a reaction onto
|
||||
* a message by id — so the stated reason is already false.
|
||||
*
|
||||
* ⚠️ But relay#46's rule is the point: **do not flip a claim because the code
|
||||
* looks present; flip it because a run proved it, and say which run.**
|
||||
* `applyEdit` was implemented and wired with nothing exercising it, because
|
||||
* `InboundControlTest` covers the codec and says in its own header that the
|
||||
* app-side application needs a `Vault` and is out of scope there. This is that
|
||||
* run.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class InboundEditAppliedTest {
|
||||
|
||||
private lateinit var server: MockWebServer
|
||||
|
||||
@BeforeTest fun start() { server = MockWebServer().also { it.start() } }
|
||||
@AfterTest fun stop() { server.shutdown() }
|
||||
|
||||
private class MapStorage : Vault.Storage {
|
||||
private val records = mutableMapOf<String, ByteArray>()
|
||||
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 fun key(): SecretKey =
|
||||
KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()
|
||||
|
||||
private val me = 1L
|
||||
private val them = 2L
|
||||
private val thread = Vault.threadForUser(2L)
|
||||
|
||||
private fun session(): Session {
|
||||
val identity = DeviceIdentity.create()
|
||||
val vault = Vault(MapStorage(), key()).also { it.saveIdentity(identity) }
|
||||
val base = server.url("/").toString()
|
||||
return Session("alice", me, identity, vault,
|
||||
RelayClient(base, "alice", "app pass word"),
|
||||
MessengerApi(base, "alice", "app pass word"))
|
||||
}
|
||||
|
||||
private fun Session.textOf(mid: String): String? =
|
||||
history(thread).firstOrNull { it.id == mid }?.text
|
||||
|
||||
@Test
|
||||
fun `an edit from the author rewrites the message it names`() {
|
||||
val s = session()
|
||||
s.record(thread, "frist post", mine = false, author = them, messageId = "m1")
|
||||
s.record(thread, "untouched", mine = false, author = them, messageId = "m2")
|
||||
|
||||
assertTrue(s.applyEdit(thread, "m1", author = them, newText = "first post"))
|
||||
assertEquals("first post", s.textOf("m1"), "the edit did not reach the transcript")
|
||||
assertEquals("untouched", s.textOf("m2"), "an edit changed a message it did not name")
|
||||
}
|
||||
|
||||
/** It survives a reload — the point is the transcript, not a live object. */
|
||||
@Test
|
||||
fun `the edit is persisted, not just applied in memory`() {
|
||||
val s = session()
|
||||
s.record(thread, "before", mine = false, author = them, messageId = "m1")
|
||||
s.applyEdit(thread, "m1", author = them, newText = "after")
|
||||
assertEquals("after", s.textOf("m1"))
|
||||
assertTrue(s.history(thread).single().editedAt != null,
|
||||
"the bubble has no way to show that it was edited")
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 **An edit from anyone but the author is refused.** Without this, one
|
||||
* contact could rewrite another's words in a transcript — which reads to
|
||||
* the person as the sender having said something they never said.
|
||||
*/
|
||||
@Test
|
||||
fun `an edit from somebody who did not write it is refused`() {
|
||||
val s = session()
|
||||
s.record(thread, "theirs", mine = false, author = them, messageId = "m1")
|
||||
|
||||
assertFalse(s.applyEdit(thread, "m1", author = 99L, newText = "forged"))
|
||||
assertEquals("theirs", s.textOf("m1"), "somebody else rewrote this message")
|
||||
}
|
||||
|
||||
/** And nobody else may rewrite what THIS device sent. */
|
||||
@Test
|
||||
fun `an edit of our own message is refused when it claims another author`() {
|
||||
val s = session()
|
||||
s.record(thread, "mine", mine = true, author = me, messageId = "m1")
|
||||
|
||||
assertFalse(s.applyEdit(thread, "m1", author = them, newText = "forged"))
|
||||
assertEquals("mine", s.textOf("m1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an edit naming a message this device does not hold changes nothing`() {
|
||||
val s = session()
|
||||
s.record(thread, "only one", mine = false, author = them, messageId = "m1")
|
||||
assertFalse(s.applyEdit(thread, "absent", author = them, newText = "x"))
|
||||
assertEquals("only one", s.textOf("m1"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user