package io.flamenet.e2e import org.json.JSONObject import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue /** * Stories and mentions, against envelopes the **iOS** build produces. * * 🔴 Every string below is the shape iOS assembles, not this engine's own * output. Two implementations that each only test against themselves agree * right up to the day somebody sends a real message — and the symptom is a * mention that never rings, or a story that arrives as "[unsupported * attachment]", with nothing in either log to say why. */ class StoryAndMentionInteropTest { // MARK: text stories private val iosTextStory = """{"fnc":1,"kind":"story","mid":"story-1","caption":"only words","exp":1800000000.0}""" @Test fun `a text story from iOS parses as a story rather than an attachment`() { val content = Content.parse(iosTextStory) assertEquals("only words", content.text) assertEquals(1800000000.0, content.textStoryExpires) assertEquals(null, content.attachment) assertEquals("story-1", content.messageId) } @Test fun `this engine assembles a text story the same way`() { val wire = Content.textStory("hello", 1800000000.0, "m-1") // ⚠️ Asserted on the WIRE STRING, not on a JSONObject built from it. // Round-tripping through JSONObject reorders the keys and puts `fnc` // somewhere other than first — which is exactly the hazard that makes // both engines hand-assemble these strings. An earlier version of this // test did that and failed, correctly. assertTrue(wire.startsWith("{\"fnc\":1"), wire) assertTrue(wire.contains("\"kind\":\"story\""), wire) assertTrue(wire.contains("\"caption\":\"hello\""), wire) assertEquals("hello", Content.parse(wire).text) } /** * ⚠️ Kotlin prints a Double in scientific notation, so this engine writes * `"exp":1.8E9` where iOS writes `1800000000.0`. Both are valid JSON and * both sides read either — but a story's expiry decides whether it shows * at all, so the shape is pinned here rather than assumed. (This is not * new: picture stories have carried `exp` this way all along.) */ @Test fun `an expiry written in scientific notation reads back`() { val wire = Content.textStory("x", 1800000000.0, "m") assertTrue(wire.contains("\"exp\":1.8E9"), wire) assertEquals(1800000000.0, Content.parse(wire).textStoryExpires) } /** 🔴 A picture story must be completely unaffected. */ @Test fun `a picture story still parses as an attachment`() { val ref = Attachment.Ref( kind = Attachment.STORY, id = "a".repeat(64), key = ByteArray(32) { 0x11 }, digest = ByteArray(32) { 0x22 }, bytes = 10, mime = "image/jpeg", width = 4, height = 3, expires = 1800000000.0, ) val content = Content.parse(Content.attachment(ref, "look")) assertNotNull(content.attachment) assertEquals(null, content.textStoryExpires) } // MARK: mentions @Test fun `mentions from iOS arrive as ids`() { val ios = """{"fnc":1,"kind":"text","caption":"morning @Ada","gid":"g1","men":[3,7]}""" val content = Content.parse(ios) assertEquals(listOf(3, 7), content.mentions) assertEquals("g1", content.groupId) } @Test fun `this engine emits mentions the same way`() { val json = Content.groupText("hi @Ada", "g1", mentions = listOf(7, 3)) assertTrue(json.contains("\"men\":[3,7]"), json) assertEquals(listOf(3, 7), Content.parse(json).mentions) } /** ⚠️ A message that merely spells a name carries no mention. */ @Test fun `spelling a name is not a mention`() { val json = Content.groupText("hello @Ada", "g1") assertFalse(json.contains("\"men\"")) assertTrue(Content.parse(json).mentions.isEmpty()) } // MARK: story reactions @Test fun `a story reaction from iOS is not mistaken for a message reaction`() { val ios = """{"fnc":1,"kind":"react","caption":"❤️","re":{"mid":"s-1","author":42,"preview":""},"st":1}""" val content = Content.parse(ios) assertTrue(content.reactsToStory) assertEquals("❤️", content.reaction) assertEquals("s-1", content.about?.messageId) } /** * 🔴 The separation that matters. Both are `kind: "react"`; without the * flag a story reaction folds onto a message with that id — and if one * ever matched, onto entirely the wrong thing. */ @Test fun `an ordinary message reaction is not read as a story one`() { val json = Content.reaction("👍", Content.Reference("m-1", 5, "")) val content = Content.parse(json) assertFalse(content.reactsToStory) assertFalse(json.contains("\"st\"")) } @Test fun `this engine emits a story reaction the same way`() { val json = Content.storyReaction("😂", "s-2", 9L) assertTrue(json.contains("\"st\":1"), json) val content = Content.parse(json) assertTrue(content.reactsToStory) assertEquals("😂", content.reaction) } // MARK: story replies @Test fun `a story reply from iOS keeps what it is about`() { val ios = """{"fnc":1,"kind":"text","mid":"m-9","caption":"lovely","re":{"mid":"s-3","author":1,"preview":"at the beach"},"st":1}""" val content = Content.parse(ios) assertTrue(content.repliesToStory) assertEquals("lovely", content.text) assertEquals("at the beach", content.about?.preview) } @Test fun `an ordinary reply is not a story reply`() { val json = Content.text("sure", messageId = "m-1", about = Content.Reference("other", 2, "earlier")) assertFalse(Content.parse(json).repliesToStory) } @Test fun `this engine emits a story reply the same way`() { val json = Content.storyReply("nice", "s-4", 3L, "sunset", "m-2") val content = Content.parse(json) assertTrue(content.repliesToStory) assertEquals("sunset", content.about?.preview) assertEquals("nice", content.text) } /** 🔴 fnc stays first, whatever else was added. */ @Test fun `every envelope this engine builds starts with fnc`() { listOf( Content.textStory("x", 1.0, "m"), Content.storyReaction("👍", "s", 1L), Content.storyReply("hi", "s", 1L, "p", "m"), Content.groupText("hi", "g", mentions = listOf(1)), ).forEach { assertTrue(it.startsWith("{\"fnc\":1"), it) } } }