package io.flamenet.e2e import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue class ContentTest { private fun ref(kind: String = Attachment.IMAGE) = Attachment.Ref( kind = kind, id = "blob-1", key = ByteArray(32) { it.toByte() }, digest = ByteArray(32) { (it * 3).toByte() }, bytes = 1234, mime = "image/jpeg", width = 800, height = 600, ) /** * โš ๏ธ The marker is a **prefix test on the bytes**, so `fnc` must be * serialised first. A JSON library that sorts keys puts `att` first, every * envelope it produces reads as plain text on the far side, and somebody's * chat fills with raw JSON. */ @Test fun `every envelope starts with the marker`() { val envelopes = listOf( Content.text("hi", messageId = "m1"), Content.groupText("hi", groupId = "g1"), Content.attachment(ref(), caption = "look"), Content.reaction("๐Ÿ‘", Content.Reference("m1", 7, "hi")), Content.callSignal(Content.CallSignal(Content.CallSignal.OFFER, "c1", sdp = "v=0")), ) envelopes.forEach { assertTrue(it.startsWith("{\"fnc\":1,"), "bad prefix: $it") } } /** * โš ๏ธ Plain 1:1 text stays raw UTF-8. A client predating content envelopes * shows an envelope as literal JSON, so the simplest message keeps the * format every client has always understood. */ @Test fun `plain text is not wrapped at all`() { assertEquals("just a message", Content.text("just a message")) assertEquals("just a message", Content.parse("just a message").text) } @Test fun `a message whose text happens to be JSON is still text`() { val tricky = """{"kind":"react","caption":"nope"}""" val parsed = Content.parse(tricky) assertEquals(tricky, parsed.text) assertNull(parsed.reaction) } /** Not an envelope, because `fnc` is not 1. Must not be reinterpreted. */ @Test fun `an envelope from a future version reads as text rather than vanishing`() { val future = """{"fnc":9,"kind":"hologram","caption":"hello"}""" assertEquals(future, Content.parse(future).text) } @Test fun `an attachment survives the round trip`() { val encoded = Content.attachment(ref(), caption = "a caption") val parsed = Content.parse(encoded) assertEquals("a caption", parsed.text) assertEquals(Attachment.IMAGE, parsed.kind) val got = parsed.attachment!! assertEquals("blob-1", got.id) assertEquals(800, got.width) assertEquals("image/jpeg", got.mime) assertContentEquals(ref().key, got.key) assertContentEquals(ref().digest, got.digest) assertTrue(parsed.isRenderable) } @Test fun `a reaction never becomes a message`() { val about = Content.Reference("m1", 7, "the original") val parsed = Content.parse(Content.reaction("๐Ÿ”ฅ", about)) assertEquals("๐Ÿ”ฅ", parsed.reaction) assertEquals("", parsed.text) assertEquals("m1", parsed.about?.messageId) assertFalse(parsed.isRenderable) // An empty emoji takes one back โ€” a message, not an absence, because // the other device has already rendered the first one. assertEquals("", Content.parse(Content.reaction("", about)).reaction) } /** * โš ๏ธ The web client sends `delivery_key`, `kt_head` and `sent` down this * same channel. A client that renders every envelope it does not recognise * shows those to the user as a bubble full of JSON. */ @Test fun `control messages are consumed rather than shown`() { for (kind in listOf("delivery_key", "kt_head", "sent")) { val parsed = Content.parse("""{"fnc":1,"kind":"$kind","key":"abc","to":3}""") assertEquals(kind, parsed.kind) assertFalse(parsed.isRenderable, "$kind would have been rendered") assertEquals("", parsed.text) // The envelope survives so a caller can act on it. assertEquals("abc", parsed.raw?.optString("key")) } } @Test fun `call signals are not messages`() { val encoded = Content.callSignal( Content.CallSignal(Content.CallSignal.OFFER, "call-9", sdp = "v=0\r\n")) val parsed = Content.parse(encoded) assertEquals("call-9", parsed.call?.callId) assertEquals("v=0\r\n", parsed.call?.sdp) assertFalse(parsed.isRenderable) } @Test fun `a reply carries its own preview`() { val about = Content.Reference("m7", 42, "what was said before") val parsed = Content.parse(Content.text("answering", messageId = "m8", about = about)) assertEquals("answering", parsed.text) assertEquals("what was said before", parsed.about?.preview) assertEquals(42, parsed.about?.author) } @Test fun `group routing survives`() { val parsed = Content.parse(Content.groupText("to everyone", groupId = "g-99")) assertEquals("g-99", parsed.groupId) assertEquals("to everyone", parsed.text) } /** * ๐Ÿ”ด Forward compatibility for control traffic. * * Control messages ride the same channel as chat and are recognised by * NAME โ€” an allowlist of kinds this build knows. A control kind from a * newer peer is in no such set, so it fell through and became a * `[unsupported attachment]` bubble: a message the user can neither read * nor act on, for something never meant for them to see. * * `ctl: 1` is what lets every kind added from here on stay quiet. */ @Test fun `a flagged unknown control kind is consumed, not rendered`() { val parsed = Content.parse("""{"fnc":1,"kind":"something-new","ctl":true}""") assertTrue(parsed.isControl, "a ctl-flagged envelope is control traffic") assertFalse(parsed.isRenderable, "and must never reach the thread") } /** * โš ๏ธ The opposite has to stay true. Unknown *content* is still shown, with * a placeholder โ€” silently dropping a real message is the one failure a * messenger cannot make, so the flag is what decides, not the unknown-ness. */ @Test fun `an unknown kind without the flag is still shown`() { val parsed = Content.parse("""{"fnc":1,"kind":"hologram"}""") assertTrue(parsed.needsNewerClient) assertFalse(parsed.isControl) assertTrue(parsed.isRenderable) } /** Kinds that predate the flag keep working without carrying it. */ @Test fun `known control kinds are still control without the flag`() { for (kind in listOf("delivery_key", "kt_head", "sent", "avatar")) { val parsed = Content.parse("""{"fnc":1,"kind":"$kind"}""") assertTrue(parsed.isControl, "$kind must stay control traffic") assertFalse(parsed.isRenderable, "$kind must not render") } } /** * Disappearing-message timers (ยง6.11). * * ๐Ÿ”ด The timer travels **inside the ratchet**. Sent any other way the relay * would learn that a conversation is timed and for how long โ€” metadata this * feature exists to avoid producing, on a product whose claim is that the * server knows as little as possible. */ @Test fun `a disappearing timer round-trips and is control traffic`() { val parsed = Content.parse(Content.disappearing(86_400)) assertEquals(Content.Kind.DISAPPEAR, parsed.kind) assertEquals(86_400, Content.disappearSeconds(parsed)) assertTrue(parsed.isControl, "the timer is never shown as a message") assertFalse(parsed.isRenderable) } /** Zero is how the timer is turned off, and must survive as zero. */ @Test fun `turning the timer off round-trips as zero`() { val parsed = Content.parse(Content.disappearing(0)) assertEquals(0, Content.disappearSeconds(parsed)) } /** * โš ๏ธ `fnc` must stay the first key. The marker is a prefix test, so a * reordered top level makes the envelope decode as a plain text message โ€” * raw JSON in somebody's conversation. */ @Test fun `the timer envelope starts with the marker`() { assertTrue(Content.disappearing(60).startsWith("""{"fnc":1,""")) } @Test fun `asking a non-timer envelope for its timer gives nothing`() { assertNull(Content.disappearSeconds(Content.parse(Content.text("hello")))) } /** * Link previews (ยง6.12). * * ๐Ÿ”ด The SENDER generates these and they ride inside the ciphertext. A card * each recipient fetched would tell the site the reader's IP, that they * received a link, and roughly when they opened it โ€” once per member on a * group message. */ @Test fun `a link preview round-trips`() { val preview = Content.LinkPreview.of("https://example.com/x", "A title", "A desc") val parsed = Content.parse(Content.text("look https://example.com/x", link = preview)) assertEquals("https://example.com/x", parsed.link?.url) assertEquals("A title", parsed.link?.title) assertEquals("look https://example.com/x", parsed.text) } /** * ๐Ÿ”ด The host comes from the URL, never the page. A card whose title claims * one destination while the link points at another is a phishing primitive. */ @Test fun `the host comes from the url not the title`() { val preview = Content.LinkPreview.of( "https://evil.example/login", "Sign in โ€” yourbank.com", "") assertEquals("evil.example", preview.host) } /** โš ๏ธ Bounded on the way in, so no renderer has to remember a cap. */ @Test fun `title and description are bounded`() { val preview = Content.LinkPreview.of( "https://example.com", "A".repeat(5_000), "B".repeat(5_000)) assertEquals(Content.LinkPreview.MAX_TITLE, preview.title.length) assertEquals(Content.LinkPreview.MAX_DESC, preview.desc.length) } /** A preview with no title is not a preview โ€” it would render as a blank card. */ @Test fun `a preview without a title is refused`() { val json = org.json.JSONObject().put("url", "https://example.com").put("title", "") assertNull(Content.LinkPreview.fromJson(json)) } /** Optional: a plain message must be byte-identical to before. */ @Test fun `a message without a preview stays raw text`() { assertEquals("plain", Content.text("plain")) assertNull(Content.parse("plain").link) } @Test fun `an unknown attachment kind shows a label rather than disappearing`() { val parsed = Content.parse("""{"fnc":1,"kind":"hologram"}""") assertEquals("[unsupported attachment]", parsed.text) assertTrue(parsed.isRenderable) } @Test fun `a malformed key in a reference is refused, not used`() { val short = """{"fnc":1,"kind":"image","att":{"id":"x","key":"AAAA","digest":"AAAA"}}""" val parsed = Content.parse(short) assertNull(parsed.attachment) assertEquals("[unsupported attachment]", parsed.text) } @Test fun `captions with quotes and newlines survive`() { val awkward = "she said \"hello\"\nand \\ left" assertEquals(awkward, Content.parse(Content.groupText(awkward, "g1")).text) } // -- Blobs --------------------------------------------------------------- @Test fun `a blob round-trips`() { val data = ByteArray(5000) { (it % 251).toByte() } val sealed = Attachment.seal(data) val reference = ref().copy(key = sealed.key, digest = sealed.digest) assertContentEquals(data, Attachment.open(sealed.blob, reference)) } @Test fun `two attachments never share a key`() { val a = Attachment.seal(ByteArray(16)) val b = Attachment.seal(ByteArray(16)) assertFalse(a.key.contentEquals(b.key)) } /** * โš ๏ธ The digest is checked BEFORE decrypting. AES-GCM would catch this too, * but failing on the digest names the problem โ€” "the relay served different * bytes" โ€” instead of reporting it as a decryption failure indistinguishable * from a broken session. */ @Test fun `a substituted blob is refused by digest, not by the cipher`() { val sealed = Attachment.seal("secret".toByteArray()) val bent = sealed.blob.copyOf().also { it[it.size / 2] = (it[it.size / 2] + 1).toByte() } val failure = assertFailsWith { Attachment.open(bent, ref().copy(key = sealed.key, digest = sealed.digest)) } assertTrue(failure.message!!.contains("digest")) } @Test fun `the wrong key does not open a blob`() { val sealed = Attachment.seal("secret".toByteArray()) assertFailsWith { Attachment.open(sealed.blob, ref().copy(key = ByteArray(32), digest = sealed.digest)) } } /** * ๐Ÿ”ด The vault persists refs through toJson and reads "exp" back out of * that very JSON โ€” a field toJson never wrote reloaded as 0, which both * rendered every story as "no expiry" and (because 0 means immortal to * isActive) meant expired story bytes were never dropped on write. */ @Test fun `a story ref keeps its expiry through the json round trip`() { val posted = ref(kind = Attachment.STORY).copy(expires = 1_700_000_000.5) val json = posted.toJson() val reloaded = Attachment.Ref.fromJson( Attachment.STORY, json, json.optDouble("exp", 0.0))!! kotlin.test.assertEquals(posted.expires, reloaded.expires) } /** Non-story refs never grow an "exp" field they did not have before. */ @Test fun `an image ref does not gain an expiry field`() { assertFalse(ref().toJson().has("exp")) } }