Files
flamenet-messenger-android/engine/src/test/kotlin/io/flamenet/e2e/StoryArchiveTest.kt
T
Paul Hitt bee63e0ef7 Keep your own stories in a private archive
Reinstates ios#54, taken by hand rather than cherry-picked. The original
commit was cut from a tree where story mute, text stories and story
replies all looked different from the versions rebuilt on this branch
today, so applying it whole would have left two implementations of each
under different names — no compile error, and exactly the kind of
divergence that rots quietly.

What came across is the archive and nothing else: the `archive()` rule,
the store, the sweep, and the setting.

🔴 The rule refuses anything not the author's own, and it does so in the
engine as well as at the caller. An archive holding somebody else's
expired story breaks the promise the app made to them, and "it is
private to you" does not repair that.

⚠️ The sweep runs at the one moment it can. Expired stories leave on
write, so if they are not caught before `saveStories` they are gone —
which is correct for everybody else's and wrong for your own.

`archiveEnabled` is injected rather than read from a context, because
this layer has no Android dependency and an unset value has to read as
OFF. This is a feature that keeps something the interface said would
disappear, so it stays off until somebody asks for it.

Vault story serialisation is now shared between the live set and the
archive rather than written twice.
2026-09-03 19:13:56 -04:00

94 lines
3.5 KiB
Kotlin

package io.flamenet.e2e
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
/**
* A private archive of your own stories (ios#54).
*
* 🔴 **This keeps something the interface said would disappear.** So it is
* opt-in, and it only ever holds stories this account posted — an archive of
* other people's expiring stories is the exact opposite of what they were
* promised, and "it is private to you" does not repair that.
*/
class StoryArchiveTest {
private fun story(id: String, mine: Boolean, at: Long) = Vault.Story(
id = id, authorId = if (mine) 1L else 7L, mine = mine,
ref = Attachment.Ref(
kind = Attachment.STORY, id = "b$id", key = ByteArray(32),
digest = ByteArray(32), bytes = 1, mime = "image/jpeg",
width = 1, height = 1, duration = 0.0, expires = 1.0,
),
caption = "c$id", postedAt = at,
)
@Test
fun `your own expired story is kept`() {
assertEquals(listOf("1"),
Vault.archive(emptyList(), listOf(story("1", true, 100))).map { it.id })
}
/** 🔴 The rule the whole feature turns on. */
@Test
fun `somebody elses expired story is never kept`() {
assertTrue(Vault.archive(emptyList(), listOf(story("2", false, 100))).isEmpty())
}
@Test
fun `a mixed batch keeps only your own`() {
val result = Vault.archive(emptyList(), listOf(
story("1", true, 100), story("2", false, 200), story("3", true, 300)))
assertEquals(setOf("1", "3"), result.map { it.id }.toSet())
}
/** ⚠️ The sweep runs on every load; without this, one story piles up. */
@Test
fun `archiving is idempotent`() {
val one = story("1", true, 100)
val first = Vault.archive(emptyList(), listOf(one))
assertEquals(1, Vault.archive(first, listOf(one)).size)
}
/** ⚠️ Newest first, so the screen needs no sort. */
@Test
fun `the archive is newest first`() {
val result = Vault.archive(emptyList(), listOf(
story("1", true, 100), story("3", true, 300), story("2", true, 200)))
assertEquals(listOf("3", "2", "1"), result.map { it.id })
}
private class MapStorage : Vault.Storage {
val records = LinkedHashMap<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() = javax.crypto.KeyGenerator.getInstance("AES")
.apply { init(256) }.generateKey()
/** ⚠️ Kept in the vault, like everything else about somebody's stories. */
@Test
fun `the archive round-trips through the vault`() {
val vault = Vault(MapStorage(), key())
assertTrue(vault.storyArchive().isEmpty())
vault.saveStoryArchive(listOf(story("1", true, 100)))
assertEquals(listOf("1"), vault.storyArchive().map { it.id })
}
/**
* 🔴 The archive must NOT be swept by the expiry rule the live set uses —
* everything in it is expired by definition, so reusing `saveStories`
* would empty it on the next write.
*/
@Test
fun `an expired story survives in the archive`() {
val vault = Vault(MapStorage(), key())
vault.saveStoryArchive(listOf(story("1", true, 100)))
assertEquals(1, vault.storyArchive().size)
}
}