Files
flamenet-messenger-android/engine/src/test/kotlin/io/flamenet/e2e/PerMessageTimerTest.kt
T
Paul HittandClaude Fable 5.1 2fa430f21c API-style comments and ktlint formatting
Every comment is reduced to its first sentence plus @param, @return
and @throws tags. Comment lines: 20040 to 8526. ktlint 1.8 applied
with its default (official Kotlin) style across all modules; 262 files
reformatted. The 264 remaining ktlint findings are naming and filename
rules, which are code changes, and are left alone.

Comments and formatting only. 1208 tests pass, counted from the XML.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 20:02:07 -04:00

220 lines
6.9 KiB
Kotlin

package io.flamenet.e2e
import org.json.JSONObject
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
/**
* A timer on one message rather than the whole conversation (§6.17).
*/
class PerMessageTimerTest {
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(): SecretKey = KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()
// ---------------------------------------------------------------- whose
// timer wins.
@Test
fun `a message timer applies when the conversation has none`() {
assertEquals(60, Vault.effectiveLifetime(message = 60, conversation = 0))
}
@Test
fun `the conversation timer applies when the message has none`() {
assertEquals(3600, Vault.effectiveLifetime(message = 0, conversation = 3600))
}
/**
* The one that matters.
*/
@Test
fun `a longer message timer cannot extend a message's life`() {
assertEquals(
3600,
Vault.effectiveLifetime(message = 604_800, conversation = 3600),
"a sender overruled the recipient's own conversation timer",
)
}
@Test
fun `a shorter message timer shortens it`() {
assertEquals(60, Vault.effectiveLifetime(message = 60, conversation = 3600))
}
@Test
fun `no timer anywhere means it does not expire`() {
assertNull(Vault.effectiveLifetime(message = 0, conversation = 0))
}
/**
* Zero means "no timer", not "expire immediately".
*/
@Test
fun `a negative timer is ignored rather than expiring immediately`() {
assertNull(Vault.effectiveLifetime(message = -1, conversation = 0))
assertEquals(3600, Vault.effectiveLifetime(message = -1, conversation = 3600))
}
// ---------------------------------------------------------------- pruning.
private fun store(
vault: Vault,
threadId: String,
at: Long,
secs: Int,
text: String,
) {
vault.append(
threadId,
JSONObject()
.put("text", text)
.put("mine", false)
.put("at", at)
.also { if (secs > 0) it.put("secs", secs) },
)
}
/**
* The bug the old code had.
*/
@Test
fun `a timed message expires even when the conversation has no timer`() {
val vault = Vault(MapStorage(), key())
val thread = Vault.threadForUser(7)
val now = 1_000_000_000L
store(vault, thread, at = now - 120_000, secs = 60, text = "should be gone")
store(vault, thread, at = now, secs = 0, text = "should stay")
val kept = vault.pruneExpired(thread, now)
assertEquals(1, kept.length())
assertEquals("should stay", kept.getJSONObject(0).getString("text"))
}
/** And the conversation timer still works on messages that carry none. */
@Test
fun `the conversation timer still expires untimed messages`() {
val vault = Vault(MapStorage(), key())
val thread = Vault.threadForUser(7)
val now = 1_000_000_000L
vault.saveDisappearSeconds(thread, 60)
store(vault, thread, at = now - 120_000, secs = 0, text = "old")
store(vault, thread, at = now, secs = 0, text = "new")
val kept = vault.pruneExpired(thread, now)
assertEquals(1, kept.length())
assertEquals("new", kept.getJSONObject(0).getString("text"))
}
/** A long message timer does not save a message from a short thread one. */
@Test
fun `a long message timer does not survive a short conversation timer`() {
val vault = Vault(MapStorage(), key())
val thread = Vault.threadForUser(7)
val now = 1_000_000_000L
vault.saveDisappearSeconds(thread, 60)
store(vault, thread, at = now - 120_000, secs = 604_800, text = "asked to live a week")
assertEquals(
0,
vault.pruneExpired(thread, now).length(),
"a sender overruled the recipient's conversation timer",
)
}
/**
* A message with no timestamp predates the field.
*/
@Test
fun `a message with no timestamp is kept`() {
val vault = Vault(MapStorage(), key())
val thread = Vault.threadForUser(7)
vault.append(
thread,
JSONObject()
.put("text", "ancient")
.put("mine", false)
.put("secs", 60),
)
assertEquals(1, vault.pruneExpired(thread, 1_000_000_000L).length())
}
// ---------------------------------------------------------------- the
// wire.
/**
* A duration, never a deadline.
*/
@Test
fun `the envelope carries a duration not a deadline`() {
val wire = Content.text("meet at 6", lifetimeSeconds = 60)
assertTrue(wire.contains("\"secs\":60"), "wrong shape: $wire")
assertEquals(60, Content.parse(wire).lifetimeSeconds)
assertFalse(wire.contains("exp"), "the envelope carries a deadline: $wire")
}
/**
* `Content.text` returns raw bytes when it has nothing to carry, so a timer
* that was not counted in that decision would be dropped and the message
* would live for ever.
*/
@Test
fun `a timed message always gets an envelope`() {
val wire = Content.text("meet at 6", lifetimeSeconds = 60)
assertTrue(wire.startsWith("{\"fnc\":1,"), "the timer was dropped: $wire")
}
@Test
fun `an untimed plain message still goes out as raw bytes`() {
assertEquals("meet at 6", Content.text("meet at 6"))
}
/**
* A conversation-timer announcement carries `secs` too and means something
* entirely different.
*/
@Test
fun `a conversation timer announcement is not a timed message`() {
val announcement = Content.parse(Content.disappearing(3600))
assertNull(
announcement.lifetimeSeconds,
"a conversation timer was read as one message's own",
)
assertEquals(3600, Content.disappearSeconds(announcement))
val timed = Content.parse(Content.text("meet at 6", lifetimeSeconds = 60))
assertNull(
Content.disappearSeconds(timed),
"a timed message was read as setting the whole conversation",
)
}
@Test
fun `a timer of zero is not written`() {
assertFalse(Content.text("hi", messageId = "m", lifetimeSeconds = 0).contains("secs"))
}
}