A file in a chat keeps its name and its label
`Ref.toJson` is what the vault persists next to a message, and it never wrote `filename`, `classification` or `compartments`. They reach the far end correctly, as additive top-level keys on the envelope, so a received document ARRIVED named. It was the local copy that lost them, on the first read back out of storage. A file in a chat therefore had a name until the app was restarted and never again, which reads as a damaged file rather than as the app forgetting. `fromJson` now defaults those three to null rather than to empty. Passing nothing means "not a wire parse, read what was stored"; passing a value, including an empty one, means the envelope said so and wins. Empty defaults could not tell those two apart, and answering "" for both is exactly what made a stored file nameless. The same rule fixes `expires`. The history path hands in 0.0, and 0 means "does not expire", so every stored story read back as immortal: its bytes were never dropped and it rendered forever. A caller that names an expiry still means it; one that does not now gets what the vault kept. They are stored under the same names the shared backup format uses, so `BackupPayload` carries them straight through. A document restored from a backup arrived with no name and no label before this, which is worse than not restoring it. Four tests on the storage round trip, one on the envelope still winning, one that an attachment which never had a name does not invent one, and one carrying a document through a backup whole. Full suite: 1276 tests, no failures. No new ktlint violations in Content.kt (3 at HEAD, 3 now). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
140affc356
commit
7ab5c774cb
@@ -0,0 +1,92 @@
|
||||
package io.flamenet.e2e
|
||||
|
||||
import org.json.JSONObject
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
/**
|
||||
* What a document is once it leaves Files: a name and a label.
|
||||
*
|
||||
* `Ref.toJson` is how an attachment is persisted alongside a message, and it
|
||||
* did not write `filename`, `classification` or `compartments`. They reached
|
||||
* the wire correctly, so a file ARRIVED with its name; it was the local copy
|
||||
* that lost it, on the first read back from the vault. A document in a chat
|
||||
* therefore had a name until the app was restarted and never again, which
|
||||
* looks like the file was damaged rather than like the app forgot.
|
||||
*/
|
||||
class AttachmentNameAtRestTest {
|
||||
private fun fileRef() =
|
||||
Attachment.Ref(
|
||||
kind = Attachment.FILE,
|
||||
id = "att-1",
|
||||
key = ByteArray(32) { 1 },
|
||||
digest = ByteArray(32) { 2 },
|
||||
bytes = 4096,
|
||||
mime = "application/octet-stream",
|
||||
filename = "contract.pdf",
|
||||
classification = 3,
|
||||
compartments = listOf(2, 7),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `a stored file keeps its name, its label and its compartments`() {
|
||||
val back = Attachment.Ref.fromJson(Attachment.FILE, fileRef().toJson(), 0.0)
|
||||
assertEquals("contract.pdf", back?.filename)
|
||||
assertEquals(3, back?.classification)
|
||||
assertEquals(listOf(2, 7), back?.compartments)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a story keeps its expiry through the vault, not only through the strip`() {
|
||||
val story =
|
||||
Attachment.Ref(
|
||||
kind = Attachment.STORY,
|
||||
id = "s-1",
|
||||
key = ByteArray(32) { 1 },
|
||||
digest = ByteArray(32) { 2 },
|
||||
bytes = 10,
|
||||
mime = "image/jpeg",
|
||||
expires = 1_700_600_000.0,
|
||||
)
|
||||
// Read back with no expiry handed in, as the history path does.
|
||||
val back = Attachment.Ref.fromJson(Attachment.STORY, story.toJson(), 0.0)
|
||||
assertEquals(1_700_600_000.0, back?.expires)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the envelope still wins, because that is where an inbound file names itself`() {
|
||||
// `filename`, `classification` and `compartments` are additive
|
||||
// top-level keys on the WIRE, not part of `att`. An inbound message is
|
||||
// parsed with them handed in explicitly, and that must keep overriding
|
||||
// whatever the attachment object does or does not carry.
|
||||
val fromWire =
|
||||
Attachment.Ref.fromJson(
|
||||
Attachment.FILE,
|
||||
fileRef().toJson(),
|
||||
0.0,
|
||||
filename = "renamed-by-sender.pdf",
|
||||
classification = 9,
|
||||
compartments = listOf(1),
|
||||
)
|
||||
assertEquals("renamed-by-sender.pdf", fromWire?.filename)
|
||||
assertEquals(9, fromWire?.classification)
|
||||
assertEquals(listOf(1), fromWire?.compartments)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an attachment that never had a name does not invent one`() {
|
||||
val plain =
|
||||
Attachment.Ref(
|
||||
kind = Attachment.IMAGE,
|
||||
id = "i-1",
|
||||
key = ByteArray(32) { 1 },
|
||||
digest = ByteArray(32) { 2 },
|
||||
bytes = 10,
|
||||
mime = "image/jpeg",
|
||||
)
|
||||
val back = Attachment.Ref.fromJson(Attachment.IMAGE, plain.toJson(), 0.0)
|
||||
assertEquals("", back?.filename)
|
||||
assertEquals(0, back?.classification)
|
||||
assertEquals(emptyList(), back?.compartments)
|
||||
}
|
||||
}
|
||||
@@ -398,4 +398,56 @@ class BackupPayloadTest {
|
||||
assertEquals("t", out.getJSONObject("groupSecrets").getJSONObject("gid-1").getString("tag"))
|
||||
assertTrue(out.getJSONArray("callLog").getJSONObject(0).getBoolean("ok"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a document keeps its name and its label across a backup`() {
|
||||
val fileAttachment =
|
||||
JSONObject()
|
||||
.put("id", "att-2")
|
||||
.put("key", "a".repeat(43))
|
||||
.put("digest", "b".repeat(43))
|
||||
.put("bytes", 900)
|
||||
.put("mime", "application/octet-stream")
|
||||
.put("filename", "contract.pdf")
|
||||
.put("classification", 3)
|
||||
.put("compartments", JSONArray(listOf(2, 7)))
|
||||
val internal =
|
||||
JSONObject().put(
|
||||
"threads",
|
||||
JSONObject().put(
|
||||
"7",
|
||||
JSONArray().put(
|
||||
JSONObject()
|
||||
.put("text", "")
|
||||
.put("at", 1L)
|
||||
.put("kind", Attachment.FILE)
|
||||
.put("att", fileAttachment),
|
||||
),
|
||||
),
|
||||
)
|
||||
val shared = BackupPayload.toShared(internal)
|
||||
val sharedAtt =
|
||||
shared
|
||||
.getJSONObject("threads")
|
||||
.getJSONArray("7")
|
||||
.getJSONObject(0)
|
||||
.getJSONObject("att")
|
||||
assertEquals(Attachment.FILE, sharedAtt.getString("kind"))
|
||||
assertEquals("contract.pdf", sharedAtt.getString("filename"))
|
||||
assertEquals(3, sharedAtt.getInt("classification"))
|
||||
|
||||
val back =
|
||||
BackupPayload
|
||||
.fromShared(shared)
|
||||
.getJSONObject("threads")
|
||||
.getJSONArray("7")
|
||||
.getJSONObject(0)
|
||||
.getJSONObject("att")
|
||||
assertEquals("contract.pdf", back.getString("filename"))
|
||||
assertEquals(3, back.getInt("classification"))
|
||||
assertEquals(
|
||||
listOf(2, 7),
|
||||
back.getJSONArray("compartments").let { a -> (0 until a.length()).map { a.getInt(it) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user